<?php
/**
* Cookie - Controle de cookies com segurança e boas práticas
* @version 2.0
*/
class Cookie {
/**
* Nome do cookie
*/
public string $name = '';
/**
* Tempo de vida em segundos (padrão: 7 dias)
*/
public int $ttl = 3600 * 24 * 7;
/**
* Opções de segurança do cookie
* secure → só trafega via HTTPS
* httponly → inacessível via JavaScript (protege contra XSS)
* samesite → protege contra CSRF (Lax cobre a maioria dos casos)
*/
public array $options = [
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
];
/**
* Dados armazenados no cookie
*/
private array $data = [];
// -------------------------------------------------------------------------
/**
* Lê o cookie existente ou cria um novo vazio
*
* O JSON vindo do cliente é validado — se vier adulterado ou inválido,
* o cookie é resetado para um array vazio.
*/
public function __construct() {
if (!empty($this->name) && isset($_COOKIE[$this->name])) {
$decoded = json_decode($_COOKIE[$this->name], true);
$this->data = is_array($decoded) ? $decoded : [];
}
}
// -------------------------------------------------------------------------
// LEITURA
// -------------------------------------------------------------------------
/**
* Verifica se uma chave existe no cookie
*
* @example
* if ($cookie->has('user_id')) { ... }
*/
public function has(string $key): bool {
return isset($this->data[$key]);
}
/**
* Retorna o valor de uma chave, ou um valor padrão se não existir
*
* @param string $key Chave a buscar
* @param mixed $default Valor retornado se a chave não existir (padrão: null)
* @return mixed
*
* @example
* $theme = $cookie->get('theme', 'light');
*/
public function get(string $key, mixed $default = null): mixed {
return $this->data[$key] ?? $default;
}
/**
* Retorna todos os dados do cookie como array associativo
*
* @example
* $all = $cookie->all();
* foreach ($all as $key => $value) { ... }
*/
public function all(): array {
return $this->data;
}
// -------------------------------------------------------------------------
// ESCRITA
// -------------------------------------------------------------------------
/**
* Adiciona ou atualiza um valor no cookie
*
* @throws RuntimeException se headers já foram enviados
*
* @example
* $cookie->add('theme', 'dark');
* $cookie->add('user_id', 42);
*/
public function add(string $key, mixed $value): void {
$this->data[$key] = $value;
$this->update();
}
/**
* Remove uma chave do cookie
* Não lança erro se a chave não existir
*
* @throws RuntimeException se headers já foram enviados
*
* @example
* $cookie->del('theme');
*/
public function del(string $key): void {
unset($this->data[$key]);
$this->update();
}
/**
* Persiste o estado atual do cookie no browser
* Chamado automaticamente por add() e del()
*
* @throws RuntimeException se headers já foram enviados
*/
public function update(): void {
$this->send($this->name, json_encode($this->data), time() + $this->ttl);
}
/**
* Remove todos os dados e apaga o cookie do browser
* Para apagar um cookie é necessário setar expires no passado
*
* @throws RuntimeException se headers já foram enviados
*
* @example
* $cookie->logout();
*/
public function logout(): void {
$this->data = [];
$this->send($this->name, '', time() - 3600);
}
// -------------------------------------------------------------------------
// INTERNO
// -------------------------------------------------------------------------
/**
* Envia o cookie com as opções de segurança configuradas
* Verifica se os headers já foram enviados antes de tentar
*
* @throws RuntimeException
*/
private function send(string $name, string $value, int $expires): void {
if (headers_sent($file, $line)) {
throw new RuntimeException(
"Não é possível definir o cookie '{$name}' — "
. "headers já enviados em {$file}:{$line}"
);
}
setcookie($name, $value, array_merge($this->options, [
'expires' => $expires,
]));
}
}
Comments