neophp / cookieconsent-package
Cookie consent banner with optional database persistence for NeoPHP
Package info
github.com/NeoPHP-Dev/neo-cookieconsent-package
pkg:composer/neophp/cookieconsent-package
Requires
- php: >=8.5
README
A cookie consent banner for NeoPHP, aligned with GDPR/ePrivacy expectations: configurable categories, cookie-based storage for anonymous visitors, and optional database persistence — with full history — when a user is logged in.
Structure
cookieconsent-package/
├── composer.json
├── README.md
├── src/
│ ├── NeoCookieConsentPackage.php
│ ├── Controllers/
│ │ └── CookieConsentController.php
│ ├── Service/
│ │ └── ConsentManager.php
│ ├── Assets/
│ │ ├── css/cookieconsent.css
│ │ └── js/cookieconsent.js
│ └── Templates/
│ └── components/
│ └── CookieConsent.macro.html.twig
├── config/
│ └── cookieconsent.config.php
└── database/
├── Entity/
│ └── CookieConsentRecord.php
├── Repository/
│ └── CookieConsentRecordRepository.php
└── Migrations/
└── MigrationVersion_CookieConsent_1.php
How storage works
- Cookie (always) — every consent choice is written to a browser cookie, readable instantly by JS without a server round-trip. This is the source of truth for anonymous visitors.
- Database (optional, one row per consent given) — if you pass a
$userType/$userIdpair when recording consent, a full history record is also persisted. Unlikeneo-twofactor-packageorneo-permissions-package, there is no unique constraint here — every consent action creates a new row, so you always have proof of what was accepted and when, even across changes.
This package never knows about your authentication system — you decide,
in your own code, whether and when to pass user identity to
ConsentManager::recordConsent().
Installation
php bin/neo package:require neophp/cookieconsent-package --project=MyProject
Register it in the project's Config/app.config.php:
return [ // ... 'packages' => [ \Vendor\NeoPHP\CookieConsentPackage\NeoCookieConsentPackage::class, ], ];
Run the migration:
php bin/neo database:migration:migrate --project=MyProject
Configuration
config/cookieconsent.config.php is copied once to
Config/Packages/CookieConsent/cookieconsent.config.php:
<?php declare(strict_types=1); return [ 'categories' => [ 'necessary' => ['label' => 'Necessary', 'description' => 'Required for the site to function.', 'required' => true], 'analytics' => ['label' => 'Analytics', 'description' => 'Helps us understand how visitors use the site.', 'required' => false], 'marketing' => ['label' => 'Marketing', 'description' => 'Used to show relevant ads.', 'required' => false], ], 'cookie_name' => 'neo_cookie_consent', 'cookie_lifetime_days' => 365, ];
A category marked required: true is always forced to true when
consent is recorded, regardless of what's submitted — it cannot be
disabled by a visitor.
Usage
Display the banner (anonymous visitors, cookie only)
public function index(ConsentManager $consent): Response { return $this->render('layout.html.twig', [ 'cookieCategories' => $consent->getCategories(), ]); }
{% import '@CookieConsent/components/CookieConsent.macro.html.twig' as CookieConsent %}
<link rel="stylesheet" href="/packages-assets/CookieConsent/css/cookieconsent.css">
{{ CookieConsent.render(cookieCategories) }}
<script src="/packages-assets/CookieConsent/js/cookieconsent.js"></script>
The banner posts to /cookie-consent/ by default (this package's own
route), which only writes the cookie — it never persists to the
database, since it has no way to know who is logged in.
Gate a third-party script on consent
window.addEventListener('neo-consent-updated', (e) => { if (e.detail.analytics) { // load Google Analytics or similar now } }); // or check at any time after the banner has resolved: if (window.NeoCookieConsent?.hasConsent('analytics')) { // ... }
Persisting consent for a logged-in user
Don't use the package's own /cookie-consent/ route for this — call
ConsentManager yourself from your own controller, where you have
access to your own auth system:
public function saveConsent(Request $request, ConsentManager $consent, AdminAuthManager $auth): JsonResponse { $categories = json_decode($request->getPost('categories', '{}'), true); $user = $auth->user(); $consent->recordConsent( $categories, $user !== null ? AdminUser::class : null, $user?->getId(), ); return $this->json(['success' => true]); }
You'd then point the macro's saveUrl parameter at your own route
instead of the default:
{{ CookieConsent.render(cookieCategories, path('my.own.consent.route')) }}
Reading consent history for a user
$repo = $em->getRepository(CookieConsentRecord::class); $history = $repo->findHistoryForUser(AdminUser::class, $userId); foreach ($history as $record) { $record->getCategories(); // array<string, bool> $record->getConsentedAt(); // \DateTime }
ConsentManager API
| Method | Purpose |
|---|---|
recordConsent(array $categories, ?string $userType = null, ?int $userId = null) |
Writes the cookie, and persists to DB if user identity is provided |
hasConsent(string $category): bool |
Reads from the cookie |
hasAnyChoice(): bool |
Whether the visitor has made any choice yet |
getCategories(): array |
The configured category definitions |
Theming
Every visual value is a CSS custom property scoped to .cc-banner:
.cc-banner { --cc-accent: #6366f1; --cc-bg: #161923; --cc-border: #2d3342; --cc-text: #e5e7eb; --cc-text-muted: #9ca3af; }
What this package does not do
- Does not automatically block or defer third-party scripts already on your page — you gate them yourself using the JS API, as shown above
- Does not scan your site for existing cookies/trackers
- Not a substitute for legal review — category definitions, wording, and whether this approach satisfies your jurisdiction's requirements is your responsibility
License
MIT