* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Josantonius\Cookie; use DateTime; use Throwable; use Josantonius\Cookie\Exceptions\CookieException; /** * Cookie handler. */ class Cookie { /** * Generates instance with a particular cookie settings configuration. * * These settings will be used to create and delete cookies. * * @param string $domain Domain for which the cookie is available. * @param int|string|DateTime $expires The time the cookie will expire. * Integers will be handled as unix time except zero. * Strings will be handled as date/time formats. * @param bool $httpOnly Access to cookie only through the HTTP protocol. * @param string $path Path where the cookie will be available. * @param bool $raw If cookie value should be sent without url encoding. * @param null|string $sameSite Enforces the use of a Lax or Strict SameSite policy. * Available values: None|Lax|Strict. * @param bool $secure Transmit cookie only over a secure HTTPS connection. * * @see https://www.php.net/manual/en/datetime.formats.php * @see https://www.php.net/manual/en/function.setcookie.php * * @throws CookieException if $sameSite value is wrong. */ public function __construct( private string $domain = '', private int|string|DateTime $expires = 0, private bool $httpOnly = false, private string $path = '/', private bool $raw = false, private null|string $sameSite = null, private bool $secure = false, ) { $this->throwExceptionIfSameSiteValueIsWrong(); } /** * Gets all cookies. * * @return array */ public function all(): array { return $_COOKIE ?? []; } /** * Checks if a cookie exists. */ public function has(string $name): bool { return isset($_COOKIE[$name]); } /** * Gets a cookie by name. * * Optionally defines a default value when the cookie does not exist. */ public function get(string $name, mixed $default = null): mixed { return $_COOKIE[$name] ?? $default; } /** * Sets a cookie by name. * * @param null|int|string|DateTime $expires The time the cookie will expire. * Integers will be handled as unix time except zero. * Strings will be handled as date/time formats. * * @see https://www.php.net/manual/en/datetime.formats.php * * @throws CookieException if headers already sent. * @throws CookieException if failure in date/time string analysis. */ public function set(string $name, mixed $value, null|int|string|DateTime $expires = null): void { $this->throwExceptionIfHeadersWereSent(); $params = [$name, $value, $this->getOptions($expires === null ? $this->expires : $expires)]; $this->raw ? setrawcookie(...$params) : setcookie(...$params); } /** * Sets several cookies at once. * * If cookies exist they are replaced, if they do not exist they are created. * * @param array $data An array of cookies. * @param null|int|string|DateTime $expires The time the cookie will expire. * Integers will be handled as unix time except zero. * Strings will be handled as date/time formats. * * @see https://www.php.net/manual/en/datetime.formats.php * * @throws CookieException if headers already sent. */ public function replace(array $data, null|int|string|DateTime $expires = null): void { $this->throwExceptionIfHeadersWereSent(); foreach ($data as $name => $value) { $this->set($name, $value, $expires); } } /** * Deletes a cookie by name and returns its value. * * Optionally defines a default value when the cookie does not exist. * * @throws CookieException if headers already sent. */ public function pull(string $name, mixed $default = null): mixed { $this->throwExceptionIfHeadersWereSent(); $value = $_COOKIE[$name] ?? $default; $this->remove($name); return $value; } /** * Deletes a cookie by name. * * @throws CookieException if headers already sent. * @throws CookieException if failure in date/time string analysis. */ public function remove(string $name): void { $this->throwExceptionIfHeadersWereSent(); $params = [$name, '', $this->getOptions(1, false)]; $this->raw ? setrawcookie(...$params) : setcookie(...$params); } /** * Deletes all cookies. * * @throws CookieException if headers already sent. */ public function clear(): void { $this->throwExceptionIfHeadersWereSent(); foreach ($_COOKIE ?? [] as $name) { $this->remove($name); } } /** * Gets cookie options. * * @throws CookieException if failure in date/time string analysis. */ private function getOptions(null|int|string|DateTime $expires, bool $formatTime = true): array { if ($formatTime) { $expires = $this->formatExpirationTime($expires); } $options = [ 'domain' => $this->domain, 'expires' => $expires, 'httponly' => $this->httpOnly, 'path' => $this->path, 'secure' => $this->secure, ]; if ($this->sameSite !== null) { $options['samesite'] = $this->sameSite; } return $options; } /** * Format the expiration time. * * @throws CookieException if failure in date/time string analysis. */ private function formatExpirationTime(int|string|DateTime $expires): int { if ($expires instanceof DateTime) { return (int) $expires->format('U'); } elseif (is_int($expires)) { return $expires; } elseif (is_string($expires)) { try { return (int) (new DateTime($expires))->format('U'); } catch (Throwable $exception) { throw new CookieException($exception->getMessage(), $exception); } } } /** * Throw exception if headers have already been sent. * * @throws CookieException if headers already sent. */ private function throwExceptionIfHeadersWereSent(): void { headers_sent($file, $line) && throw new CookieException(sprintf( 'The headers have already been sent in "%s" at line %d.', $file, $line )); } /** * Throw exception if $sameSite value is wrong. * * @throws CookieException if $sameSite value is wrong. */ private function throwExceptionIfSameSiteValueIsWrong(): void { $values = ['none', 'lax', 'strict']; if ($this->sameSite && !in_array(strtolower($this->sameSite), $values)) { throw new CookieException( 'Invalid value for the sameSite param. Available values: ' . implode('|', $values) ); } } }