delight-im/cookie
Modern cookie management for PHP. A clean, secure API for setting, reading, and deleting cookies, with support for SameSite, HttpOnly and Secure flags, cookie prefixes, and session handling—helping you follow browser security best practices without boilerplate.
Install and confirm it loads
composer require delight-im/cookie
There is no service provider and no config file to publish — it's a plain library. Laravel's Composer autoloader picks it up immediately, so the first thing to do is open vendor/delight-im/PHP-Cookie/src/Cookie.php and read the class signature before writing any code. The public surface you will actually use is small:
Cookie::set($name, $value, $options) — write a cookie (and make it visible in $_COOKIE for the rest of the request).Cookie::get($name) / Cookie::has($name) (also exposed as exists()) — read from $_COOKIE.Cookie::remove($name, $options) — expire a cookie.Cookie::setDefaults($options) — set process-wide defaults for path/domain/secure/httpOnly/sameSite.Cookie::SameSiteStrict / SameSiteLax / Cookie::SameSiteNone constants.Where to look first
Cookie::setDefaults() — this is the highest-leverage call in the library. Set it once and every subsequent Cookie::set() inherits safe attributes.$options array keys accepted by set() (path, domain, secure, httponly, samesite, expires) — these map almost 1:1 onto Laravel's config/session.php keys, which makes migration between the two mental models easy.Session class if it's present in your installed version — it's a separate concern from Cookie and you almost certainly should not swap Laravel's session driver for it.First real use case
The realistic first use case for a Laravel developer is not replacing Illuminate\Cookie\CookieJar. It's a legacy or framework-free corner of the app (an old /billing script, a shared package used by both a Laravel app and a plain PHP app, a webhook landing page outside the framework) that needs cookies with SameSite and HttpOnly set correctly. Wire the defaults once in AppServiceProvider::boot():
use Delight\Cookie\Cookie;
public function boot(): void
{
Cookie::setDefaults([
'path' => '/',
'secure' => $this->app->environment('production'),
'httponly' => true,
'samesite' => Cookie::SameSiteLax,
]);
}
Then Cookie::set('legacy_pref', 'dark') from that legacy route behaves correctly without you hand-rolling setcookie().
Defaults-in-a-provider, reads at the edge. Centralise setDefaults() in a service provider, but keep reads close to where they are used. Because Cookie::get() reads the $_COOKIE superglobal directly, it is a hard dependency on global state — wrapping it in a tiny injectable class (App\Support\LegacyCookies) keeps your controllers testable and gives you one place to swap implementations later if you outgrow the package.
Middleware for header emission. If a cookie must be set for every request (a locale hint, an A/B bucket), do it in a middleware's handle() before returning $next($request). Laravel has not flushed headers at that point, so the library's header emission works. Never call Cookie::set() from terminate() or from a queued job — headers are already gone and you'll get "headers already sent" warnings under some SAPIs.
Keep it out of Laravel's cookie pipeline. The cleanest pattern is strict segregation by cookie name prefix:
| Concern | Owner |
|---|---|
Session, CSRF (XSRF-TOKEN), "remember me" |
Laravel (EncryptCookies, CookieJar) |
legacy_* / third-party integration cookies |
Delight\Cookie\Cookie |
Add the same prefixes to App\Http\Middleware\EncryptCookies::$except so Laravel's encrypter never touches values it didn't write, and vice versa. Mixing the two on the same cookie name produces values Laravel can't decrypt and Cookie::get() can't verify.
Deletion symmetry. Cookie::remove($name, $options) only works if path, domain, and secure match the original set() exactly. The robust pattern is to define option arrays as named constants or a small enum and pass the same array to both calls:
final class LegacyCookieScope
{
public const OPTIONS = ['path' => '/legacy', 'secure' => true, 'httponly' => true, 'samesite' => 'Lax'];
}
Cookie::set('legacy_token', $token, LegacyCookieScope::OPTIONS);
Cookie::remove('legacy_token', LegacyCookieScope::OPTIONS);
Signed-payload pattern. The library gives you attributes, not integrity. If the value is user-influenced (a redirect target, a plan id), base64-encode a JSON payload plus an HMAC and verify with hash_equals() on read. Treat Cookie::get() output as fully attacker-controlled.
Bridging into Laravel responses. When you need the cookie to appear on a specific Response object rather than the global header bag, read the value out and re-emit it through Laravel ($response->cookie(...) or Cookie::queue() from the facade) so it participates in testing assertions like assertCookie(). The reverse — taking Laravel's queued cookies and pushing them through this package — is not worth the effort.
Testing. $this->withCookie('legacy_pref', 'dark') populates the request cookie bag; Cookie::get() looks at $_COOKIE, so set the superglobal directly in the test ($_COOKIE['legacy_pref'] = 'dark';) or, better, inject your wrapper and fake it. For assertions on what was written, inspect the response headers for Set-Cookie rather than reading $_COOKIE back.
Do not use this for sessions. Even if the package ships a Session class with hardened defaults, swapping it in for Laravel's session driver bypasses the database/redis drivers, the StartSession middleware, CSRF integration, and Auth. The gain (SameSite, HttpOnly) is already available via config/session.php in any modern Laravel version.
Cookie::get() is not $request->cookie(). It ignores Laravel's EncryptCookies middleware and the request object entirely. Values you set through Laravel's facade will look like ciphertext garbage (or be absent) to this library, and values you set here will fail decryption if Laravel tries to read them. Decide which layer owns each name and document it.
secure => true will silently break local development. Browsers on http://localhost drop Secure cookies. Gate it on app()->environment('production') or on $request->isSecure(), and never hardcode true in a defaults array that ships to dev machines.
Path/domain mismatches are the #1 cause of "the cookie doesn't delete." A cookie set for / cannot be removed by a call scoped to /legacy, and a cookie set on example.com is a different cookie from one set on .example.com. Also remember that a Domain attribute cannot be set for localhost in some browsers.
__Host- and __Secure- prefixes enforce themselves. If you adopt __Host-session, the browser rejects the cookie unless it also has Secure, Path=/, and no Domain. That's a useful hard guarantee, but it makes the option array non-negotiable — a stray domain key turns the cookie into a silent no-op. Verify with the browser devtools' Application → Cookies panel, not with var_dump($_COOKIE).
Size and encoding limits are inherited from setcookie(). ~4KB per cookie, and the browser caps cookies per domain. Values are percent-encoded on the wire, so round-tripping JSON or base64 is fine but raw UTF-8 with unusual characters can surprise you — encode explicitly rather than relying on the library. Large cookies are the usual culprit when you see intermittent "session randomly logged out" reports in production.
Headers timing. Setting a cookie from a controller is fine because Laravel emits headers during response send. Setting one from a console command, a queue worker, an Artisan-scheduled task, or ob_start-adjacent legacy code will warn or silently do nothing.
Name collision with the Cookie facade alias. Laravel registers Cookie as a facade alias. In a controller with default imports, Cookie::get(...) can resolve to Illuminate\Support\Facades\Cookie instead of the package — a genuinely confusing failure mode (no exception, just wrong data). Use the fully qualified \Delight\Cookie\Cookie::set(...) or use Delight\Cookie\Cookie as LegacyCookie;.
Version pinning and risk. 166 stars, no dependents, and a low-opportunity score mean this is a small, single-maintainer library with no Laravel integration surface. Pin the major version in composer.json, read the changelog when bumping, and prefer Illuminate\Cookie for anything that touches auth or sessions. The package's durable value is as a framework-agnostic cookie layer in code that must also run outside Laravel — if that constraint doesn't apply to you, the built-in cookie jar does everything this does.
Debugging checklist when a cookie "isn't there": (1) did Cookie::set() run before headers were sent? (2) does Secure/SameSite=None reject it in this browser context? (3) does the path in the request URL fall under the cookie's path? (4) is the value too large? (5) is it actually a different cookie because of the domain? Check the Set-Cookie header in devtools' Network tab first — it answers 1–3 and 5 in one glance.
How can I help you explore Laravel packages today?