core45/laravel-cookieconsent
Composer 安装命令:
composer require core45/laravel-cookieconsent
包简介
Laravel wrapper for orestbida/cookieconsent: PHP-first config, lang-file i18n, server-side consent reads, script gating, iframemanager, and a database consent audit trail.
README 文档
README
A Laravel wrapper for orestbida/cookieconsent v3 and its companion iframemanager. It gives you a PHP-first configuration file instead of a hand-written JS blob, i18n through normal Laravel lang files, server-side reads of what the visitor consented to, Blade-driven script gating, and a database audit trail of every consent decision whose model write path is append-only.
The package bundles orestbida/cookieconsent 3.1.0 and iframemanager 1.3.0, served self-hosted from
resources/dist / the published vendor/cookieconsent assets — no CDN request is made. Upstream updates are
re-vendored and checksum-verified by the maintainers, then shipped as a new package release; there is no
auto-update mechanism, so pin the package version if you need a specific upstream JS version.
| Capability | What you get |
|---|---|
| Configuration | A single config/cookieconsent.php array, passed through to CookieConsent.run() almost verbatim |
| Translations | Standard Laravel lang files (lang/vendor/cookieconsent/{locale}/cookieconsent.php), active or all locales |
| Script gating | @cookiescript Blade directives that emit blocked <script type="text/plain"> tags |
| Server-side reads | @consent/Consent/CookieConsent facade + a consent:* route middleware |
| Audit trail | A consent_logs table with an append-only model write path, export/prune Artisan commands, optional Filament resource |
| Media consent | An @iframemanager / @iframe() integration for YouTube-style click-to-load embeds |
| Security | Optional CSP nonce injection for every script tag the package renders |
Installation
composer require core45/laravel-cookieconsent php artisan vendor:publish --tag=cookieconsent-config php artisan vendor:publish --tag=cookieconsent-assets php artisan migrate
Other publishable tags, used only if you need to customize them:
php artisan vendor:publish --tag=cookieconsent-lang # lang/vendor/cookieconsent/{locale}/*.php php artisan vendor:publish --tag=cookieconsent-views # resources/views/vendor/cookieconsent/*.blade.php php artisan vendor:publish --tag=cookieconsent-migrations # copy the migration into database/migrations
Add @cookieconsent to your layout's <head>. This is the recommended placement — the banner script waits for
DOMContentLoaded internally, so it's safe to load it before the rest of the page:
<head> ... @cookieconsent </head>
The package auto-registers its consent cookie (cookieconsent.cookie.name, default cc_cookie) as an exception to
Laravel's EncryptCookies middleware on boot. You do not need to edit bootstrap/app.php yourself.
Configuration
config/cookieconsent.php is published as a plain PHP array. Every key except translations_mode, csp_nonce,
logging, and iframemanager is passed straight through to CookieConsent.run() — see the
configuration reference for every
option orestbida/cookieconsent understands.
return [ // Passed through to CookieConsent.run() — see the configuration reference linked above. 'cookie' => [ 'name' => 'cc_cookie', 'expiresAfterDays' => 182, // WARNING: enabling `useLocalStorage` disables server-side consent // reads (@consent, Consent::has(), consent:* middleware). See below. ], 'mode' => 'opt-in', 'revision' => 0, 'guiOptions' => [ 'consentModal' => ['layout' => 'box', 'position' => 'bottom left'], 'preferencesModal' => ['layout' => 'box'], ], 'categories' => [ 'necessary' => ['enabled' => true, 'readOnly' => true], 'analytics' => [ 'autoClear' => [ 'cookies' => [ // Use CookieConsent::regex('^_ga') for RegExp matching. ['name' => '_gid'], ], ], ], ], // Package-only keys, stripped before the array reaches the JS config: 'translations_mode' => 'active', // 'active' = current locale only, 'all' = every published locale 'csp_nonce' => null, // see "Content Security Policy" below 'logging' => [ /* ... */ ], // see "Consent audit trail" below 'iframemanager' => [ /* ... */ ], // see "iframemanager" below ];
RegExp cookie matching
autoClear.cookies[].name normally does an exact string match. To emit a real JS RegExp instead, use the
CookieConsent::regex() helper — it returns a sentinel array that the package's Blade view revives into
new RegExp(...) client-side:
use Core45\CookieConsent\Facades\CookieConsent; 'autoClear' => [ 'cookies' => [ ['name' => CookieConsent::regex('^_ga')], // matches _ga, _ga_ABC123, ... ['name' => CookieConsent::regex('^_gid$', 'i')], // second arg is JS RegExp flags ], ],
A plain string (['name' => '_gid']) is always an exact match.
Translations
Publish the language files and edit them like any other Laravel translation:
php artisan vendor:publish --tag=cookieconsent-lang
lang/vendor/cookieconsent/en/cookieconsent.php
lang/vendor/cookieconsent/es/cookieconsent.php
lang/vendor/cookieconsent/en/iframemanager.php
lang/vendor/cookieconsent/es/iframemanager.php
The package ships en and es out of the box. cookieconsent.php maps directly onto orestbida's
language.translations.{locale} shape (consentModal, preferencesModal, etc); iframemanager.php supplies the
per-service loadBtn/notice/... strings.
config('cookieconsent.translations_mode') controls how many locales are sent to the browser:
'active'(default) — only the currentapp()->getLocale()locale is embedded.'all'— every published locale directory is embedded, so client-side locale switches (CookieConsent.setLanguage()) work without a page reload.
Script gating
@cookiescript blocks a <script> tag until the visitor has consented to its category, using orestbida's
type="text/plain" data-category="..." convention.
External script:
@cookiescript('analytics', 'https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX')
Inline script:
@cookiescript('analytics') window.dataLayer = window.dataLayer || []; function gtag(){ dataLayer.push(arguments); } gtag('js', new Date()); gtag('config', 'G-XXXXXXX'); @endcookiescript
You can also negate a category to run cleanup code when consent is not given (or is withdrawn), and pass extra HTML attributes as a third argument:
@cookiescript('!analytics') // runs when analytics consent is absent @endcookiescript @cookiescript('analytics', 'https://example.com/ga.js', ['data-service' => 'Google Analytics'])
Blade quirk: don't glue
@endcookiescriptdirectly onto a word character with no whitespace or punctuation between them — Blade's compiler won't recognizesomeCode@endcookiescriptas a directive boundary. This is rarely an issue in practice because inline scripts almost always end in;,}, or a newline before the closing directive, as in the examples above.
Server-side consent
Read what the visitor has consented to from PHP, without waiting for JavaScript:
@consent('analytics') <p>Analytics-only content.</p> @endconsent
use Core45\CookieConsent\Support\Consent; use Core45\CookieConsent\Facades\CookieConsent; app(Consent::class)->has('analytics'); // bool CookieConsent::has('analytics'); // same, via the facade CookieConsent::categories(); // list<string>, e.g. ['necessary', 'analytics'] CookieConsent::acceptType(); // 'all' | 'necessary' | 'custom' | null (no cookie yet)
Gate a whole route behind a category with the consent:* middleware — it aborts with 403 when consent is missing:
Route::get('/dashboard/analytics', AnalyticsController::class) ->middleware('consent:analytics');
Not an authorization boundary.
@consent,Consent::has(), and theconsent:*middleware all read the unencryptedcc_cookie(it's excluded fromEncryptCookiesbecause orestbida/cookieconsent needs to read/write it as plaintext client-side). A visitor can freely edit that cookie's contents. Treat these checks as a UX/ compliance gate that reflects the visitor's own stated preference — never as access control for sensitive resources; use real authorization (policies, gates, middleware backed by server-side state) for that.
useLocalStoragewarning. If you setcookie.useLocalStoragetotruein the config, orestbida/cookieconsent stores consent in the browser'slocalStorageinstead of a cookie. That value never reaches the server, so@consent/@endconsent,Consent::has(),CookieConsent::has(), and theconsent:*middleware will always report "no consent" — they degrade silently tofalse/[]/null, except for a one-timeLog::warning()the first time a read is attempted (guarded by a static flag, so it fires once per PHP process — once per request under classic PHP-FPM, once total under Octane). LeaveuseLocalStorageoff (the default) if you rely on any server-side consent check.
Consent audit trail
Every accept/change interaction the banner fires (cc:onFirstConsent, cc:onChange) is POSTed to an internal
cookieconsent.log route and written as a new row in consent_logs. The ConsentLog model's write path is
append-only: it throws if you try to update an existing row through the model. This guard is model-instance-level
only — it does not stop raw DB::table('consent_logs')->update(...), a mass ConsentLog::where(...)->update(...),
or delete(), all of which bypass Eloquent's saving hook. For a hard guarantee, restrict UPDATE/DELETE grants on
the consent_logs table at the database user level, or add a database trigger that rejects them outright.
Hardening the audit trail at the database level
The model guard only covers writes made through Eloquent instances. For a guarantee that holds even against raw
SQL, DB::table(...), or a compromised app process, revoke UPDATE/DELETE on consent_logs from the
application's database user and grant it only SELECT/INSERT:
-- MySQL REVOKE UPDATE, DELETE ON consent_logs FROM 'app_user'@'%'; -- PostgreSQL REVOKE UPDATE, DELETE ON consent_logs FROM app_user;
The cookieconsent:prune command deletes expired rows, so if you apply this hardening, run pruning under a
separate, more privileged database role/connection instead of the app's normal user.
| Column | Contents |
|---|---|
consent_id, revision |
orestbida's own consent identifier and config revision |
action |
first_consent or change |
accept_type |
all, custom, or necessary |
accepted_categories, rejected_categories, accepted_services |
JSON arrays |
language_code |
locale active on the banner at the time of consent |
policy_version, policy_hash |
your configured version string, and a canonicalized sha256 of the effective config — see CookieConsent::policyHash() |
ip_address, user_agent |
evidentiary metadata, subject to the PII config below |
user_type, user_id |
polymorphic link to the authenticated user, if any |
payload |
the full raw cookie payload, for future-proofing |
Evidentiary fields (ip_address, user_agent, user_type/user_id, policy_hash) are always derived server-side
from the request — they are never trusted from client input.
Delivery. The consent-log POST does not block or affect the banner UI. It retries transient failures (network errors,
429,5xx) up to twice with backoff, and each event carries a server-checked idempotency key so a retried or duplicated delivery never creates a second row. If the tab closes or navigates away before the request completes, the pending payload is sent as a last-chancenavigator.sendBeacon()call onpagehide/tab- hidden. Non-retryable client errors (other4xx) and exhausted retries are logged to the browser console, not retried further.
PII configuration
'logging' => [ 'enabled' => true, 'csrf' => true, 'capture_ip' => 'raw', // 'raw' | 'hashed' | false 'ip_hash_salt' => env('COOKIECONSENT_IP_SALT'), 'capture_user_agent' => true, 'link_user' => true, 'morph_id_type' => 'int', // 'int' | 'uuid' | 'ulid' | 'string' — must match your User key type 'policy_version' => null, 'retention_days' => null, // null disables pruning 'rate_limit' => '30,1', // Laravel throttle expression applied to the log endpoint ],
Legal note. Storing an IP address and/or user agent alongside a consent decision is itself an act of processing personal data, even though the purpose is to prove consent. Document this processing activity in your own privacy policy, and tune
capture_ip('hashed'orfalse) andretention_daysto match your data minimization obligations under GDPR/LOPD-GDD or the regime that applies to you. This package stores evidence; it does not decide your retention policy for you.
Security note.
capture_ip => 'raw'/'hashed'relies on$request->ip(), whose accuracy depends on your application'strustProxiesconfiguration (bootstrap/app.php→->withMiddleware()->trustProxies(...), orApp\Http\Middleware\TrustProxieson older skeletons). Behind a load balancer, reverse proxy, or CDN, an unconfigured trusted-proxy list means every request is logged with the proxy's IP, not the visitor's.
Retrieval
php artisan cookieconsent:export --format=csv > consent-evidence.csv php artisan cookieconsent:export --format=json --user=42 php artisan cookieconsent:export --format=json --user=42 --user-type="App\\Models\\Admin" php artisan cookieconsent:export --consent-id=abc123 --from=2026-01-01 --to=2026-06-30
--to accepts a date-only value (as above) and treats it as inclusive of the whole day — a row created at
2026-06-30 23:00 is included, not excluded by an implicit midnight bound. --user alone matches user_id only;
it is not scoped by user_type, so ids can collide across different morph types (e.g. an Admin and a
Customer both with id 42). Pass --user-type alongside --user to scope the match to a specific morph class.
--format only accepts json or csv; anything else fails with an error and a non-zero exit code.
The underlying ConsentLog model exposes matching Eloquent scopes:
ConsentLog::forConsentId('abc123')->get(); ConsentLog::forUser($user)->get(); ConsentLog::between('2026-01-01', '2026-06-30 23:59:59')->get(); // or, with Carbon: ConsentLog::between('2026-01-01', now()->parse('2026-06-30')->endOfDay())->get(); ConsentLog::latestPerConsentId()->get(); // one row per consent_id, the most recent
between()bounds are used verbatim. Unlike the export command's--to, thebetween()scope passes its bounds straight towhereBetween('created_at', [$from, $to])— it does not auto-expand a date-only end bound to end-of-day.ConsentLog::between('2026-01-01', '2026-06-30')excludes anything after2026-06-30 00:00:00. Pass an explicit time ('2026-06-30 23:59:59') or a CarbonendOfDay()instance to include the whole day.
Retention
php artisan cookieconsent:prune
Deletes rows older than logging.retention_days. It's a no-op (logs an info message, deletes nothing) when
retention_days is null. Schedule it if you want automatic pruning:
// routes/console.php Schedule::command('cookieconsent:prune')->daily();
Filament plugin
An optional, read-only ConsentLogResource is available if filament/filament (^5.0) is installed. Register it
on any panel:
use Core45\CookieConsent\Filament\CookieConsentFilamentPlugin; $panel->plugin(CookieConsentFilamentPlugin::make());
The resource has no create/edit pages — consent logs are evidence, not editable records.
iframemanager
Gate third-party embeds (YouTube, Vimeo, maps, ...) behind a consent category with a click-to-load placeholder, using iframemanager.
'iframemanager' => [ 'enabled' => true, 'services' => [ 'youtube' => [ 'category' => 'analytics', // which cookieconsent category unlocks this service 'embedUrl' => 'https://www.youtube-nocookie.com/embed/{data-id}', 'thumbnailUrl' => 'https://i3.ytimg.com/vi/{data-id}/hqdefault.jpg', 'iframe' => [ 'allow' => 'accelerometer; autoplay; encrypted-media', ], ], ], ],
Place @iframemanager near @cookieconsent (typically right after it):
@cookieconsent @iframemanager
Then drop a placeholder wherever you'd normally embed the iframe:
@iframe('youtube', $videoId, ['params' => 'rel=0', 'thumbnail' => $customThumbnailUrl])
Per-service copy (loadBtn, notice, ...) comes from lang/vendor/cookieconsent/{locale}/iframemanager.php, keyed by
service name (youtube in the example above), just like the main cookieconsent.php lang file.
Advanced JS / custom callbacks
Two ways to hook into the banner's behavior beyond what the config array exposes:
1. cc:* window events — orestbida/cookieconsent dispatches cc:onFirstConsent, cc:onConsent, cc:onChange,
cc:onModalReady, cc:onModalShow, and cc:onModalHide on window. Listen to them from any script on the page,
inline or bundled:
<script> window.addEventListener('cc:onConsent', () => { if (window.CookieConsent.acceptedCategory('analytics')) { // ... } }); </script>
2. The cookieconsent:config push stack — runs after the revived config JS variable is declared but
before CookieConsent.run(config) is called, so you can mutate config in place (e.g. make a static option
dynamic):
@push('cookieconsent:config') <script> config.cookie.expiresAfterDays = () => 365; </script> @endpush
Google Consent Mode recipe
Combining both mechanisms to wire up Google Consent Mode v2:
@push('cookieconsent:config') <script> window.dataLayer = window.dataLayer || []; function gtag() { dataLayer.push(arguments); } gtag('consent', 'default', { analytics_storage: 'denied', ad_storage: 'denied' }); window.addEventListener('cc:onConsent', () => { if (window.CookieConsent.acceptedCategory('analytics')) { gtag('consent', 'update', { analytics_storage: 'granted' }); } }); </script> @endpush
Content Security Policy
If your app runs under a strict CSP, set csp_nonce to a callable that returns the per-request nonce:
'csp_nonce' => fn () => \Illuminate\Support\Facades\Vite::cspNonce(),
Every inline and external <script> tag the package itself renders (@cookieconsent, @iframemanager) gets a
nonce="..." attribute automatically.
CSP caveat. Scripts you push via
@push('cookieconsent:config')are plain Blade/HTML — they do not automatically inherit the configured nonce. Under a strict CSP you must addnonce="..."to your own pushed<script>tags yourself, e.g.<script nonce="{{ $nonce }}">...</script>.
Versioning & License
This package follows Semantic Versioning. It is open-sourced under the MIT license, Copyright (c) 2026 core45.
统计信息
- 总下载量: 0
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 1
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-07-09