Skip to content

Superglobales, sesiones y cookies ​

PHP olvida todo al terminar cada petición. Entonces, ¿cómo sabe una tienda que sigues con la sesión iniciada o qué hay en tu carrito? En esta lección conocerás las superglobales, aprenderás a usar sesiones y cookies, y construirás un login básico, mensajes flash y un carrito de compras.

Concepto ​

Superglobales ​

Son arrays que PHP llena automáticamente en cada petición y que están disponibles en cualquier parte del código (incluso dentro de funciones):

SuperglobalContiene
$_GETParámetros de la URL (?id=5)
$_POSTDatos de formularios enviados por POST
$_FILESArchivos subidos (lección 13)
$_SERVERInformación del servidor y de la petición
$_SESSIONDatos de la sesión del usuario
$_COOKIECookies enviadas por el navegador
$_REQUESTMezcla de GET, POST y COOKIE (evítala: no sabes de dónde vino el dato)
$_ENVVariables de entorno

$_SERVER: datos de la petición ​

php
<?php
echo $_SERVER['REQUEST_METHOD'];  // GET, POST, PUT, DELETE...
echo $_SERVER['REQUEST_URI'];     // /productos.php?pagina=2
echo $_SERVER['SCRIPT_NAME'];     // /productos.php
echo $_SERVER['HTTP_HOST'];       // localhost o misitio.com
echo $_SERVER['REMOTE_ADDR'];     // IP del visitante
echo $_SERVER['HTTP_USER_AGENT'] ?? ''; // Navegador del usuario
echo $_SERVER['HTTPS'] ?? 'off';  // 'on' si es HTTPS (depende del servidor)

// Ruta limpia sin query string
$ruta = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

Error común

Confiar ciegamente en $_SERVER['HTTP_*'] (como HTTP_REFERER o HTTP_USER_AGENT): esas cabeceras las envía el navegador y el usuario puede falsificarlas. Úsalas para estadísticas, nunca para seguridad.

Sesiones frente a cookies ​

Imagina el guardarropa de un evento:

  • Entregas tu abrigo y te dan un boleto con un número. El boleto es la cookie de sesión (se queda contigo, en el navegador).
  • El abrigo se queda guardado en el guardarropa. Eso son los datos de sesión (se quedan en el servidor).
  • Cada vez que vuelves, muestras el boleto y te reconocen.
CookieSesión
Dónde se guardaEn el navegador del usuarioEn el servidor
¿El usuario puede verla/modificarla?SíNo (sólo ve el ID)
Tamaño~4 KBPrácticamente ilimitado
DuraciónLa que tú definasHasta cerrar el navegador o expirar
Úsala paraPreferencias (tema, idioma), "recordarme"Login, carrito, mensajes flash

Sesiones ​

Lo básico ​

php
<?php
// Siempre al inicio, ANTES de imprimir cualquier cosa
session_start();

// Guardar
$_SESSION['usuario_id'] = 42;
$_SESSION['nombre'] = 'Ana';

// Leer (con valor por defecto)
$nombre = $_SESSION['nombre'] ?? 'Invitado';

// Borrar un dato
unset($_SESSION['nombre']);

// Comprobar
if (isset($_SESSION['usuario_id'])) {
    echo 'Hay sesión iniciada';
}

session_start() hace dos cosas: si el navegador no trae cookie de sesión, crea un ID nuevo y se lo envía; si la trae, carga los datos guardados en $_SESSION.

Error común

Warning: session_start(): Session cannot be started after headers have already been sent. Llamaste a session_start() después de imprimir algo (HTML, un espacio, un echo, o un BOM en el archivo). Colócalo en la primera línea, antes de cualquier salida.

Configuración segura de la sesión ​

php
<?php
// bootstrap.php — inclúyelo al inicio de cada página
declare(strict_types=1);

session_start([
    'cookie_httponly' => true,   // JavaScript no puede leer la cookie (mitiga XSS)
    'cookie_secure'   => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off', // Sólo por HTTPS
    'cookie_samesite' => 'Lax',  // No se envía en peticiones POST desde otros sitios (mitiga CSRF)
    'use_strict_mode' => true,   // Rechaza IDs de sesión inventados
    'gc_maxlifetime'  => 7200,   // Los datos expiran tras 2 h de inactividad
]);

Lo profundizamos en Sesiones seguras.

Ejemplo: login básico con sesión ​

Nota

Aquí los usuarios están en un array para centrarnos en las sesiones. En la lección de PDO los leerás de MySQL. Las contraseñas se guardan siempre con password_hash, nunca en texto plano.

php
<?php
declare(strict_types=1);

session_start([
    'cookie_httponly' => true,
    'cookie_samesite' => 'Lax',
    'use_strict_mode' => true,
]);

function e(?string $t): string
{
    return htmlspecialchars($t ?? '', ENT_QUOTES, 'UTF-8');
}

function usuarioActual(): ?array
{
    return $_SESSION['usuario'] ?? null;
}

/** Protege una página: si no hay sesión, manda al login. */
function requiereLogin(): void
{
    if (usuarioActual() === null) {
        header('Location: login.php');
        exit;
    }
}

/** Guarda un mensaje para mostrarlo en la siguiente página. */
function flash(string $tipo, string $mensaje): void
{
    $_SESSION['flash'][] = ['tipo' => $tipo, 'mensaje' => $mensaje];
}

/** Devuelve los mensajes flash y los borra (se muestran una sola vez). */
function obtenerFlash(): array
{
    $mensajes = $_SESSION['flash'] ?? [];
    unset($_SESSION['flash']);
    return $mensajes;
}
php
<?php
require __DIR__ . '/bootstrap.php';

// Usuarios de ejemplo. El hash se generó con: password_hash('secreto123', PASSWORD_DEFAULT)
$usuarios = [
    'ana@correo.com' => [
        'id' => 1,
        'nombre' => 'Ana',
        'rol' => 'admin',
        'hash' => password_hash('secreto123', PASSWORD_DEFAULT), // En la vida real viene de la BD
    ],
];

$error = null;
$email = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $email = trim($_POST['email'] ?? '');
    $password = $_POST['password'] ?? '';
    $usuario = $usuarios[$email] ?? null;

    // password_verify compara la contraseña con el hash de forma segura
    if ($usuario && password_verify($password, $usuario['hash'])) {
        session_regenerate_id(true); // Nuevo ID de sesión al iniciar: evita "session fixation"

        $_SESSION['usuario'] = [
            'id' => $usuario['id'],
            'nombre' => $usuario['nombre'],
            'rol' => $usuario['rol'],
        ];
        flash('exito', "¡Bienvenida, {$usuario['nombre']}!");
        header('Location: panel.php');
        exit;
    }

    // Mensaje genérico: no revelamos si el correo existe o no
    $error = 'Correo o contraseña incorrectos.';
}
?>
<!DOCTYPE html>
<html lang="es">
<head><meta charset="UTF-8"><title>Iniciar sesión</title></head>
<body>
    <h1>Iniciar sesión</h1>
    <?php if ($error): ?><p style="color:red"><?= e($error) ?></p><?php endif; ?>
    <form method="post">
        <input name="email" type="email" placeholder="Correo" value="<?= e($email) ?>" required>
        <input name="password" type="password" placeholder="Contraseña" required>
        <button>Entrar</button>
    </form>
</body>
</html>
php
<?php
require __DIR__ . '/bootstrap.php';
requiereLogin(); // Si no hay sesión, aquí se corta y redirige

$usuario = usuarioActual();
?>
<!DOCTYPE html>
<html lang="es">
<head><meta charset="UTF-8"><title>Panel</title></head>
<body>
    <?php foreach (obtenerFlash() as $f): ?>
        <p class="<?= e($f['tipo']) ?>"><?= e($f['mensaje']) ?></p>
    <?php endforeach; ?>

    <h1>Panel de <?= e($usuario['nombre']) ?></h1>
    <p>Rol: <?= e($usuario['rol']) ?></p>

    <form method="post" action="logout.php">
        <button>Cerrar sesión</button>
    </form>
</body>
</html>
php
<?php
require __DIR__ . '/bootstrap.php';

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405); // Método no permitido
    exit;
}

// 1. Vaciar los datos
$_SESSION = [];

// 2. Borrar la cookie de sesión del navegador
if (ini_get('session.use_cookies')) {
    $p = session_get_cookie_params();
    setcookie(session_name(), '', time() - 42000, $p['path'], $p['domain'], $p['secure'], $p['httponly']);
}

// 3. Destruir la sesión en el servidor
session_destroy();

header('Location: login.php');
exit;

Buena práctica

Cierra sesión con un formulario POST, no con un enlace GET. Un enlace GET a logout.php puede activarse desde cualquier sitio con una simple imagen, desconectando a tus usuarios.

Ejemplo: carrito de compras en sesión ​

php
<?php
// carrito.php
require __DIR__ . '/bootstrap.php';

$catalogo = [
    1 => ['nombre' => 'Taza', 'precio' => 120],
    2 => ['nombre' => 'Playera', 'precio' => 280],
    3 => ['nombre' => 'Gorra', 'precio' => 210],
];

// El carrito guarda [id_producto => cantidad]
$_SESSION['carrito'] ??= [];

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $id = (int) ($_POST['id'] ?? 0);
    $accion = $_POST['accion'] ?? '';

    if (isset($catalogo[$id])) {
        match ($accion) {
            'agregar' => $_SESSION['carrito'][$id] = ($_SESSION['carrito'][$id] ?? 0) + 1,
            'quitar'  => (function () use ($id) {
                if (isset($_SESSION['carrito'][$id]) && --$_SESSION['carrito'][$id] <= 0) {
                    unset($_SESSION['carrito'][$id]);
                }
            })(),
            default => null,
        };
    }
    if ($accion === 'vaciar') {
        $_SESSION['carrito'] = [];
    }

    header('Location: carrito.php'); // PRG
    exit;
}

$total = 0;
foreach ($_SESSION['carrito'] as $id => $cantidad) {
    $total += $catalogo[$id]['precio'] * $cantidad;
}
?>
<h2>Catálogo</h2>
<?php foreach ($catalogo as $id => $p): ?>
    <form method="post" style="display:inline-block; margin:.5rem">
        <input type="hidden" name="id" value="<?= $id ?>">
        <?= e($p['nombre']) ?> ($<?= $p['precio'] ?>)
        <button name="accion" value="agregar">Agregar</button>
    </form>
<?php endforeach; ?>

<h2>Tu carrito</h2>
<?php if (!$_SESSION['carrito']): ?>
    <p>Vacío.</p>
<?php else: ?>
    <ul>
    <?php foreach ($_SESSION['carrito'] as $id => $cantidad): ?>
        <li>
            <?= e($catalogo[$id]['nombre']) ?> × <?= $cantidad ?>
            = $<?= number_format($catalogo[$id]['precio'] * $cantidad, 2) ?>
            <form method="post" style="display:inline">
                <input type="hidden" name="id" value="<?= $id ?>">
                <button name="accion" value="quitar">−</button>
            </form>
        </li>
    <?php endforeach; ?>
    </ul>
    <p><strong>Total: $<?= number_format($total, 2) ?></strong></p>
    <form method="post"><button name="accion" value="vaciar">Vaciar carrito</button></form>
<?php endif; ?>

Observa que en la sesión guardamos sólo IDs y cantidades; los precios siempre se toman del catálogo del servidor. Si guardaras el precio en un campo oculto del formulario, un usuario podría cambiarlo.

Cookies ​

php
<?php
// Crear una cookie que dura 30 días
setcookie('tema', 'oscuro', [
    'expires'  => time() + 60 * 60 * 24 * 30, // Marca de tiempo de expiración
    'path'     => '/',                        // Disponible en todo el sitio
    'secure'   => true,                       // Sólo por HTTPS (en localhost puede ir en false)
    'httponly' => true,                       // Invisible para JavaScript
    'samesite' => 'Lax',
]);

// Leer (¡disponible hasta la SIGUIENTE petición!)
$tema = $_COOKIE['tema'] ?? 'claro';

// Lista blanca: el usuario puede modificar la cookie a su antojo
$tema = in_array($tema, ['claro', 'oscuro'], true) ? $tema : 'claro';

// Borrar: misma cookie con fecha en el pasado
setcookie('tema', '', ['expires' => time() - 3600, 'path' => '/']);

Error común

Esperar ver la cookie en $_COOKIE justo después de setcookie(). La cookie se envía al navegador en la respuesta y éste la devuelve en la siguiente petición. Además, setcookie() envía una cabecera, así que debe llamarse antes de cualquier salida.

Nunca guardes datos sensibles en cookies

Ni contraseñas, ni roles (rol=admin), ni precios. El usuario puede editar sus cookies con las herramientas del navegador. Para algo como "soy admin" usa la sesión, que vive en el servidor.

Ejercicios ​

  1. Crea un contador de visitas por usuario usando la sesión: "Has visitado esta página N veces".
  2. Crea un selector de idioma (es/en) que guarde la preferencia en una cookie de 1 año y muestre un saludo en el idioma elegido.
  3. Agrega al login un cierre de sesión automático tras 15 minutos de inactividad (pista: guarda $_SESSION['ultima_actividad'] = time()).
Ver solución
php
<?php
session_start();
$_SESSION['visitas'] = ($_SESSION['visitas'] ?? 0) + 1;
echo "Has visitado esta página {$_SESSION['visitas']} veces.";
php
<?php
$idiomas = ['es' => '¡Hola! Bienvenido', 'en' => 'Hello! Welcome'];

if (isset($_GET['idioma']) && array_key_exists($_GET['idioma'], $idiomas)) {
    setcookie('idioma', $_GET['idioma'], [
        'expires' => time() + 60 * 60 * 24 * 365,
        'path' => '/',
        'httponly' => true,
        'samesite' => 'Lax',
    ]);
    header('Location: ' . strtok($_SERVER['REQUEST_URI'], '?')); // Recarga sin el parámetro
    exit;
}

$idioma = $_COOKIE['idioma'] ?? 'es';
$idioma = array_key_exists($idioma, $idiomas) ? $idioma : 'es';
?>
<p><?= $idiomas[$idioma] ?></p>
<a href="?idioma=es">Español</a> | <a href="?idioma=en">English</a>
  1. En bootstrap.php, después de session_start():
php
<?php
const INACTIVIDAD_MAX = 15 * 60; // 15 minutos en segundos

if (isset($_SESSION['usuario'])) {
    $ultima = $_SESSION['ultima_actividad'] ?? time();
    if (time() - $ultima > INACTIVIDAD_MAX) {
        $_SESSION = [];
        session_destroy();
        session_start();
        flash('aviso', 'Tu sesión expiró por inactividad.');
        header('Location: login.php');
        exit;
    }
    $_SESSION['ultima_actividad'] = time();
}

(Define la función flash() antes de este bloque o mueve este código al final del archivo.)

Resumen ​

  • Las superglobales ($_GET, $_POST, $_SERVER, $_SESSION, $_COOKIE...) están disponibles en todo el código.
  • No confíes en cabeceras HTTP_*: el usuario las controla.
  • session_start() va antes de cualquier salida; los datos se guardan en el servidor y el navegador sólo tiene un ID.
  • Al iniciar sesión, llama a session_regenerate_id(true); al cerrarla, vacía, borra la cookie y destruye.
  • Los mensajes flash se guardan en la sesión y se borran al mostrarse.
  • Las cookies viven en el navegador: úsalas sólo para preferencias y siempre valida su contenido.
  • Configura cookies con httponly, secure y samesite.

Academia Web — aprende, copia y construye.