Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .horde.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ provides:
psr/http-client-implementation: ^1.0.3
dependencies:
required:
php: ^7.4 || ^8
php: ^8.1
composer:
horde/exception: ^3
horde/support: ^3
Expand Down
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
"role": "lead"
}
],
"time": "2026-05-25",
"time": "2026-06-10",
"repositories": [],
"require": {
"php": "^7.4 || ^8",
"php": "^8.1",
"horde/exception": "^3 || dev-FRAMEWORK_6_0",
"horde/support": "^3 || dev-FRAMEWORK_6_0",
"psr/http-message": "^2",
Expand Down
93 changes: 93 additions & 0 deletions src/Cookie.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php

declare(strict_types=1);

/**
* Copyright 2026 Horde LLC (http://www.horde.org/)
*
* See the enclosed file LICENSE for license information (BSD). If you
* did not receive this file, see http://www.horde.org/licenses/bsd.
*
* @category Horde
* @license http://www.horde.org/licenses/bsd BSD-2-Clause
* @package Http
*/

namespace Horde\Http;

use DateTimeImmutable;

/**
* HTTP cookie value object.
*
* Models a single cookie according to RFC 6265 / RFC 6265bis. The interface
* is the public contract; {@see StrictCookie} is the framework's default
* implementation. Alternative implementations (looser validation, signed
* payloads, etc.) implement this interface directly or wrap a default
* instance.
*
* Implementations MUST be immutable. Every with*() method returns a fresh
* instance with the requested change applied.
*
* The formatter side is delegated to {@see CookieParser}; implementations
* call into it from {@see toSetCookieHeader()} and {@see toCookieHeader()}.
*/
interface Cookie
{
public function name(): string;

public function value(): string;

public function expires(): ?DateTimeImmutable;

public function maxAge(): ?int;

public function domain(): ?string;

public function path(): string;

public function secure(): bool;

public function httpOnly(): bool;

public function sameSite(): SameSite;

public function withValue(string $value): self;

public function withExpires(?DateTimeImmutable $expires): self;

public function withMaxAge(?int $maxAge): self;

public function withDomain(?string $domain): self;

public function withPath(string $path): self;

public function withSecure(bool $secure = true): self;

public function withHttpOnly(bool $httpOnly = true): self;

public function withSameSite(SameSite $sameSite): self;

/**
* Render as a Set-Cookie header value (server-side emission).
*
* Example: "name=value; Path=/; HttpOnly; SameSite=Lax"
*/
public function toSetCookieHeader(): string;

/**
* Render as a single name=value pair for a Cookie request header
* (client-side emission). No attributes; just the pair.
*/
public function toCookieHeader(): string;

/**
* Return a "delete this cookie" cookie matching this one's scope.
*
* Path and Domain are preserved; Max-Age is set to 0; the value is
* cleared. Browsers see the result as a deletion request iff the
* Path and Domain match the original Set-Cookie that established
* the cookie they hold.
*/
public function deletion(): self;
}
90 changes: 90 additions & 0 deletions src/CookieList.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

declare(strict_types=1);

/**
* Copyright 2026 Horde LLC (http://www.horde.org/)
*
* See the enclosed file LICENSE for license information (BSD). If you
* did not receive this file, see http://www.horde.org/licenses/bsd.
*
* @category Horde
* @license http://www.horde.org/licenses/bsd BSD-2-Clause
* @package Http
*/

namespace Horde\Http;

/**
* Typed view over a request's parsed cookies.
*
* Shaped to wrap the result of PSR-7's
* {@see \Psr\Http\Message\ServerRequestInterface::getCookieParams()}
* (a flat associative array of name => value), and to parse a raw
* Cookie request header string into the same shape via
* {@see fromCookieHeader()}.
*
* Read-only. The instance is the request side of the cookie story; if
* you want to emit cookies on a response, build {@see Cookie} instances
* and use the server-side glue (`Horde\Http\Server\Cookies::with()`).
*
* Repeated names: last-wins, matching the convention browsers use when
* emitting cookies in path-most-specific order.
*/
final class CookieList
{
/** @param array<string, string> $cookies */
public function __construct(private readonly array $cookies) {}

/**
* Build from PSR-7's getCookieParams() output. Pass-through
* constructor; provided for symmetry with fromCookieHeader().
*
* @param array<string, string> $params
*/
public static function fromCookieParams(array $params): self
{
return new self($params);
}

/**
* Parse a Cookie request header value into a list.
*
* Empty/whitespace-only headers produce an empty list.
*/
public static function fromCookieHeader(string $header): self
{
return new self(CookieParser::parseCookieHeader($header));
}

public function get(string $name): ?string
{
return $this->cookies[$name] ?? null;
}

public function has(string $name): bool
{
return array_key_exists($name, $this->cookies);
}

/**
* @return list<string>
*/
public function names(): array
{
return array_keys($this->cookies);
}

/**
* @return array<string, string>
*/
public function toArray(): array
{
return $this->cookies;
}

public function count(): int
{
return count($this->cookies);
}
}
Loading
Loading