Introducción a PHP desde cero: qué es, cómo funciona un servidor web y cómo ejecutar tu primer archivo

PHP es uno de los lenguajes más utilizados para desarrollar aplicaciones web del lado del servidor. Antes de aprender variables, condicionales o formularios, conviene entender dónde se ejecuta PHP, qué papel cumple un servidor web y qué ocurre realmente cuando escribimos una URL en el navegador.

En esta guía vamos a construir esa base teórica desde cero y después vamos a ejecutar nuestro primer archivo PHP.

¿Qué es PHP?

PHP es un lenguaje de programación que normalmente se ejecuta en el servidor.

Eso significa que el navegador no interpreta directamente el código PHP. El servidor ejecuta ese código y devuelve como resultado HTML, JSON, texto u otro contenido.

Por ejemplo, este código:

<?php
echo 'Hola desde PHP';

no llega al navegador tal como está escrito. El servidor ejecuta PHP y el navegador recibe algo equivalente a:

Hola desde PHP

Cliente y servidor

En una aplicación web existen, como mínimo, dos partes importantes:

  • cliente: normalmente el navegador;
  • servidor: la máquina o software que procesa las solicitudes y devuelve respuestas.

Cuando escribimos una dirección como:

https://misitio.com/productos.php

el navegador envía una petición al servidor. El servidor busca el recurso, ejecuta PHP si corresponde y devuelve una respuesta.

Flujo básico de una aplicación PHP

Navegador
   ↓ HTTP
Servidor web (Apache / Nginx)
   ↓
PHP
   ↓
MySQL u otra base de datos
   ↓
PHP genera respuesta
   ↓
HTML / JSON
   ↓
Navegador

No todas las aplicaciones usan exactamente este flujo, pero es una buena representación inicial.

¿Qué es un servidor web?

Un servidor web es el software encargado de recibir solicitudes HTTP y devolver respuestas.

Dos de los más utilizados son:

  • Apache HTTP Server;
  • Nginx.

También PHP incluye un servidor de desarrollo integrado que resulta útil para proyectos simples y pruebas locales.

Apache

Apache es un servidor web muy utilizado en entornos PHP.

En instalaciones locales como XAMPP, Apache suele escuchar en el puerto 80. Por eso podemos acceder mediante:

http://localhost/

Si tenemos un archivo:

C:\xampp\htdocs\club\index.php

podemos acceder normalmente con:

http://localhost/club/index.php

Nginx

Nginx también es muy utilizado en servidores Linux y entornos de producción.

En este caso suele trabajar junto con PHP-FPM, que es quien ejecuta los archivos PHP.

El flujo simplificado es:

Navegador → Nginx → PHP-FPM → PHP → respuesta

Servidor integrado de PHP

PHP puede iniciar un servidor de desarrollo sin Apache ni Nginx.

Desde una terminal podemos ejecutar:

php -S 127.0.0.1:8000

Luego accedemos desde el navegador a:

http://127.0.0.1:8000/

o:

http://localhost:8000/

¿Qué significa localhost?

localhost representa la computadora en la que estamos trabajando.

Normalmente apunta a la dirección IP:

127.0.0.1

Por eso estas URLs suelen referirse a la misma máquina:

http://localhost/
http://127.0.0.1/

¿Qué es un puerto?

Una misma computadora puede ofrecer varios servicios al mismo tiempo. Los puertos permiten identificar cuál servicio debe recibir una conexión.

Algunos puertos frecuentes son:

  • 80: HTTP;
  • 443: HTTPS;
  • 8000: muy usado para servidores de desarrollo;
  • 3306: puerto habitual de MySQL.

Por ejemplo:

http://localhost:8000/

significa que queremos conectarnos al servicio web que escucha en el puerto 8000 de nuestra propia máquina.

HTTP y HTTPS

HTTP es el protocolo utilizado para intercambiar información entre navegador y servidor.

HTTPS es HTTP protegido mediante cifrado TLS.

Ejemplos:

http://localhost/
https://clubprogramador.com/

En producción deberíamos utilizar HTTPS siempre que sea posible.

¿Qué ocurre cuando escribimos una URL?

Supongamos que ingresamos:

http://localhost/club/index.php

De forma simplificada ocurre lo siguiente:

  1. el navegador interpreta la URL;
  2. se conecta al servidor web;
  3. solicita el recurso /club/index.php;
  4. Apache detecta que es un archivo PHP;
  5. PHP ejecuta el código;
  6. se genera una respuesta;
  7. Apache devuelve esa respuesta al navegador;
  8. el navegador muestra el resultado.

Una petición HTTP real

Conceptualmente, el navegador puede enviar algo parecido a:

GET /club/index.php HTTP/1.1
Host: localhost

Y el servidor podría responder:

HTTP/1.1 200 OK
Content-Type: text/html

<h1>Hola</h1>

¿Qué diferencia hay entre HTML y PHP?

HTML es un lenguaje de marcado. Describe la estructura de una página.

<h1>Hola mundo</h1>

PHP es un lenguaje de programación que puede generar HTML dinámicamente.

<?php
$nombre = 'Ana';
echo '<h1>Hola ' . $nombre . '</h1>';

¿Qué diferencia hay entre PHP y JavaScript?

Tradicionalmente:

  • PHP se ejecuta en el servidor;
  • JavaScript se ejecuta principalmente en el navegador.

JavaScript también puede ejecutarse en servidores mediante tecnologías como Node.js, pero en una arquitectura PHP tradicional cumple principalmente funciones del lado del cliente.

¿Qué papel cumple MySQL?

PHP puede comunicarse con MySQL para almacenar y recuperar información.

Por ejemplo:

PHP → consulta SQL → MySQL → resultado → PHP

PHP puede usar ese resultado para generar HTML o una respuesta JSON.

XAMPP

XAMPP es un paquete que instala varias herramientas útiles para desarrollo local, entre ellas normalmente:

  • Apache;
  • PHP;
  • MariaDB/MySQL;
  • phpMyAdmin.

En Windows, el directorio web suele ser:

C:\xampp\htdocs\

Si creamos:

C:\xampp\htdocs\miweb\index.php

podemos abrirlo mediante:

http://localhost/miweb/

Laragon

Laragon es otra herramienta popular para desarrollo PHP en Windows.

Su carpeta habitual de proyectos es:

C:\laragon\www\

Por ejemplo:

C:\laragon\www\club\index.php

puede abrirse normalmente mediante:

http://localhost/club/

Dependiendo de la configuración, Laragon también puede generar dominios locales automáticos, por ejemplo:

http://club.test/

DocumentRoot

El DocumentRoot es el directorio desde el cual el servidor web publica archivos.

En XAMPP suele ser:

C:\xampp\htdocs

En servidores Linux con Apache puede ser algo como:

/var/www/html

En Nginx también se configura un directorio raíz mediante su configuración.

Por qué no funciona abrir un PHP con doble clic

Si hacemos doble clic sobre:

C:\xampp\htdocs\club\index.php

Windows puede intentar abrir el archivo directamente.

Pero PHP necesita ser ejecutado por un intérprete del lado del servidor.

Por eso debemos acceder mediante una URL:

http://localhost/club/index.php

Nuestro primer archivo PHP

Creamos un archivo llamado:

index.php

y escribimos:

<?php

echo 'Hola mundo desde PHP';

Luego lo abrimos mediante la URL correspondiente.

La etiqueta <?php

Indica que comienza código PHP.

<?php

En archivos que contienen solamente PHP normalmente no es necesario escribir la etiqueta de cierre ?>.

echo

echo envía contenido a la salida.

echo 'Hola';

También podemos generar HTML:

echo '<h1>Hola mundo</h1>';

Mezclar PHP y HTML

PHP puede convivir dentro de un documento HTML.


    Mi primera página PHP

<h1>Mi sitio</h1>

<?php
echo 'Contenido generado con PHP';
?>

Archivos .php y .html

Un archivo .html normalmente es servido directamente.

Un archivo .php debe pasar por el intérprete PHP antes de ser enviado al navegador.

Por eso un archivo PHP puede generar contenido dinámico.

Códigos de estado HTTP

El servidor también devuelve un código que informa qué ocurrió.

  • 200: respuesta correcta;
  • 404: recurso no encontrado;
  • 403: acceso prohibido;
  • 500: error interno del servidor.

Errores comunes al comenzar

Apache no está iniciado

Si intentamos acceder a localhost y no existe ningún servidor escuchando, el navegador no podrá conectarse.

Puerto ocupado

Otro programa puede estar utilizando el puerto 80. En ese caso Apache puede no iniciar o puede configurarse otro puerto.

Por ejemplo:

http://localhost:8080/

Archivo fuera del DocumentRoot

Guardar:

C:\Users\Usuario\Desktop\index.php

no significa que Apache pueda acceder automáticamente a ese archivo.

Escribir una URL incorrecta

La ruta física:

C:\xampp\htdocs\club\index.php

no se escribe directamente en el navegador. Su URL es:

http://localhost/club/index.php

PHP no está instalado o configurado

Si usamos el servidor integrado y ejecutamos:

php -S localhost:8000

pero el comando php no existe, debemos instalar PHP o agregar su ejecutable al PATH del sistema.

Ejercicio práctico

Creá una carpeta llamada:

php-practica

dentro del directorio web de XAMPP o Laragon.

Después creá:

index.php

con el siguiente contenido:


    
    PHP

<h1>Mi primer proyecto PHP</h1>

<p>
<?php
    echo 'PHP está funcionando correctamente';
?>
</p>

Luego probalo desde:

http://localhost/php-practica/

Qué aprendimos

  • qué es PHP;
  • diferencia entre cliente y servidor;
  • qué hace un servidor web;
  • qué son Apache y Nginx;
  • qué significa localhost;
  • qué es un puerto;
  • diferencia entre PHP, HTML, JavaScript y MySQL;
  • cómo funcionan XAMPP y Laragon;
  • qué es DocumentRoot;
  • cómo ejecutar un archivo PHP;
  • cómo funciona echo.

¿Qué sigue?

En la próxima guía vamos a estudiar variables, tipos de datos y operadores en PHP, que son la base para empezar a escribir programas con lógica.

¿Te sirvió esta guía? ☕

Si este contenido te ayudó y querés apoyar a Club Programador para seguir publicando ejercicios, proyectos y guías gratuitas, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal

20 ejercicios avanzados de PHP resueltos: POO, MySQL, seguridad y MVC

Estos 20 ejercicios avanzados de PHP resueltos están pensados para quienes ya dominan sintaxis, formularios, PDO y bases de datos y quieren practicar conceptos de aplicaciones reales.

Vas a trabajar con POO, seguridad, autenticación, roles, consultas preparadas, MVC y Composer.

1. Crear una clase Producto

<?php
class Producto
{
    public function __construct(
        private string $nombre,
        private float $precio
    ) {
    }

    public function getNombre(): string
    {
        return $this->nombre;
    }

    public function getPrecio(): float
    {
        return $this->precio;
    }
}
?>

2. Encapsular el stock de un producto

public function descontarStock(int $cantidad): void
{
    if ($cantidad <= 0 || $cantidad > $this->stock) {
        throw new InvalidArgumentException('Stock inválido');
    }

    $this->stock -= $cantidad;
}

3. Crear una interfaz de notificaciones

interface Notificable
{
    public function enviar(string $mensaje): void;
}

4. Implementar notificaciones por email

class EmailNotificacion implements Notificable
{
    public function enviar(string $mensaje): void
    {
        echo 'Email: ' . $mensaje;
    }
}

5. Crear un repositorio con PDO

class ProductoRepository
{
    public function __construct(
        private PDO $pdo
    ) {
    }

    public function todos(): array
    {
        return $this->pdo
            ->query('SELECT * FROM productos')
            ->fetchAll(PDO::FETCH_ASSOC);
    }
}

6. Buscar por ID con una consulta preparada

public function buscar(int $id): ?array
{
    $stmt = $this->pdo->prepare(
        'SELECT * FROM productos WHERE id = :id'
    );

    $stmt->execute(['id' => $id]);

    $resultado = $stmt->fetch(PDO::FETCH_ASSOC);

    return $resultado ?: null;
}

7. Implementar una transacción

$pdo->beginTransaction();

try {
    // operaciones relacionadas

    $pdo->commit();
} catch (Throwable $e) {
    $pdo->rollBack();
    throw $e;
}

8. Login seguro con password_verify()

if (
    $usuario &&
    password_verify($password, $usuario['password'])
) {
    session_regenerate_id(true);

    $_SESSION['usuario_id'] = $usuario['id'];
}

9. Middleware simple de autenticación

function requireLogin(): void
{
    if (!isset($_SESSION['usuario_id'])) {
        header('Location: /login.php');
        exit;
    }
}

10. Controlar acceso por rol

function requireAdmin(): void
{
    requireLogin();

    if ($_SESSION['rol'] !== 'admin') {
        http_response_code(403);
        exit('Acceso denegado');
    }
}

11. Generar un token CSRF

if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

12. Validar el token CSRF

if (
    !isset($_POST['csrf_token']) ||
    !hash_equals(
        $_SESSION['csrf_token'],
        $_POST['csrf_token']
    )
) {
    http_response_code(403);
    exit;
}

13. Crear una capa de servicio

class ProductoService
{
    public function __construct(
        private ProductoRepository $repository
    ) {
    }

    public function listarActivos(): array
    {
        return array_filter(
            $this->repository->todos(),
            fn($producto) => $producto['activo']
        );
    }
}

14. Crear un controlador MVC

class ProductoController
{
    public function __construct(
        private ProductoService $service
    ) {
    }

    public function index(): void
    {
        $productos = $this->service->listarActivos();

        require '../Views/productos/index.php';
    }
}

15. Crear un router simple

$ruta = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

switch ($ruta) {
    case '/productos':
        $controller->index();
        break;

    default:
        http_response_code(404);
}

16. Usar namespaces

<?php

namespace App\Services;

class ProductoService
{
}
?>

17. Configurar autoload PSR-4

{
    "autoload": {
        "psr-4": {
            "App\\": "app/"
        }
    }
}

Después:

composer dump-autoload

18. Crear una búsqueda paginada con PDO

$buscar = trim($_GET['buscar'] ?? '');
$pagina = max(1, (int) ($_GET['pagina'] ?? 1));
$limite = 20;
$offset = ($pagina - 1) * $limite;

$stmt = $pdo->prepare(
    'SELECT *
     FROM productos
     WHERE nombre LIKE :buscar
     ORDER BY id DESC
     LIMIT :limite OFFSET :offset'
);

$stmt->bindValue(':buscar', '%' . $buscar . '%');
$stmt->bindValue(':limite', $limite, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();

19. Registrar errores en un archivo

try {
    // operación
} catch (Throwable $e) {
    error_log(
        $e->getMessage() . PHP_EOL,
        3,
        __DIR__ . '/app.log'
    );
}

20. Diseñar un mini sistema MVC completo

Objetivo: construir una aplicación de gestión de productos con:

  • login;
  • roles;
  • CRUD;
  • PDO;
  • consultas preparadas;
  • CSRF;
  • arquitectura MVC;
  • autoload PSR-4;
  • validación;
  • paginación y búsqueda.

Una estructura posible:

app/
├── Controllers/
├── Models/
├── Repositories/
├── Services/
└── Views/

config/
public/
vendor/
composer.json

¿Qué deberías poder hacer después?

Si podés resolver estos desafíos entendiendo cada parte, ya tenés una base muy sólida de PHP moderno y estás en condiciones de comenzar a trabajar con frameworks como Laravel con mucha más claridad.

Serie de ejercicios de PHP

También podés ver todos los contenidos prácticos en la sección Ejercicios resueltos.

¿Te sirvió esta guía? ☕

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal

20 ejercicios intermedios de PHP resueltos: arrays, funciones, formularios y PDO

Si ya dominás variables, condicionales, bucles y funciones, estos 20 ejercicios intermedios de PHP resueltos te van a ayudar a combinar conceptos y acercarte a problemas reales.

1. Filtrar números pares de un array

<?php
$numeros = [1,2,3,4,5,6,7,8];

$pares = array_filter($numeros, fn($n) => $n % 2 === 0);

print_r($pares);
?>

2. Ordenar productos por precio

<?php
$productos = [
    ['nombre' => 'Mouse', 'precio' => 15000],
    ['nombre' => 'Monitor', 'precio' => 180000],
    ['nombre' => 'Teclado', 'precio' => 25000]
];

usort($productos, fn($a, $b) => $a['precio'] <=> $b['precio']);

print_r($productos);
?>

3. Buscar un usuario por email

<?php
$usuarios = [
    ['nombre' => 'Ana', 'email' => 'ana@mail.com'],
    ['nombre' => 'Juan', 'email' => 'juan@mail.com']
];

$emailBuscado = 'juan@mail.com';

$resultado = array_filter($usuarios, fn($u) => $u['email'] === $emailBuscado);

print_r($resultado);
?>

4. Eliminar duplicados

<?php
$valores = [1,2,2,3,4,4,5];

$unicos = array_unique($valores);

print_r($unicos);
?>

5. Contar palabras de un texto

<?php
$texto = 'PHP es simple de aprender y muy utilizado';

echo str_word_count($texto);
?>

6. Validar un email

<?php
$email = 'usuario@email.com';

if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
    echo 'Email válido';
} else {
    echo 'Email inválido';
}
?>

7. Validar un formulario POST

<?php
$nombre = trim($_POST['nombre'] ?? '');

if ($nombre === '') {
    echo 'El nombre es obligatorio';
}
?>

8. Mantener valores del formulario

<input
    type="text"
    name="nombre"
    value="<?= htmlspecialchars($_POST['nombre'] ?? '') ?>"
>

9. Generar una contraseña aleatoria

<?php
$caracteres = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$password = '';

for ($i = 0; $i < 12; $i++) {
    $password .= $caracteres[random_int(0, strlen($caracteres) - 1)];
}

echo $password;
?>

10. Guardar una contraseña con password_hash()

<?php
$password = 'ClaveSegura123';

$hash = password_hash($password, PASSWORD_DEFAULT);

echo $hash;
?>

11. Comprobar una contraseña

<?php
if (password_verify($password, $hash)) {
    echo 'Contraseña correcta';
}
?>

12. Crear una sesión de usuario

<?php
session_start();

$_SESSION['usuario_id'] = 10;
$_SESSION['nombre'] = 'Ana';
?>

13. Proteger una página con sesión

<?php
session_start();

if (!isset($_SESSION['usuario_id'])) {
    header('Location: login.php');
    exit;
}
?>

14. Leer un archivo de texto

<?php
$contenido = file_get_contents('datos.txt');

echo nl2br(htmlspecialchars($contenido));
?>

15. Guardar datos en un archivo

<?php
$texto = "Nueva línea
";

file_put_contents(
    'datos.txt',
    $texto,
    FILE_APPEND
);
?>

16. Conectarse a MySQL con PDO

<?php
$pdo = new PDO(
    'mysql:host=localhost;dbname=tienda;charset=utf8mb4',
    'root',
    ''
);

$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
?>

17. Buscar un registro con una consulta preparada

<?php
$stmt = $pdo->prepare(
    'SELECT * FROM productos WHERE id = :id'
);

$stmt->execute([
    'id' => 5
]);

$producto = $stmt->fetch(PDO::FETCH_ASSOC);
?>

18. Insertar un producto con PDO

<?php
$stmt = $pdo->prepare(
    'INSERT INTO productos (nombre, precio)
     VALUES (:nombre, :precio)'
);

$stmt->execute([
    'nombre' => 'Teclado',
    'precio' => 25000
]);
?>

19. Crear una paginación básica

<?php
$pagina = max(1, (int) ($_GET['pagina'] ?? 1));
$porPagina = 10;
$offset = ($pagina - 1) * $porPagina;

$stmt = $pdo->prepare(
    'SELECT * FROM productos LIMIT :limite OFFSET :offset'
);

$stmt->bindValue(':limite', $porPagina, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
?>

20. Crear un filtro de búsqueda

<?php
$buscar = trim($_GET['buscar'] ?? '');

$stmt = $pdo->prepare(
    'SELECT * FROM productos
     WHERE nombre LIKE :buscar'
);

$stmt->execute([
    'buscar' => '%' . $buscar . '%'
]);

$productos = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>

¿Qué sigue?

Si podés resolver estos ejercicios sin copiar las soluciones, ya estás listo para trabajar con desafíos avanzados: POO, seguridad, relaciones entre clases, arquitectura MVC y proyectos completos.

Serie de ejercicios de PHP

También podés ver todos los contenidos prácticos en la sección Ejercicios resueltos.

¿Te sirvió esta guía? ☕

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal

20 ejercicios de PHP para principiantes resueltos paso a paso

Si estás aprendiendo PHP desde cero, practicar es una de las mejores formas de fijar conceptos. En esta guía vas a encontrar 20 ejercicios de PHP para principiantes resueltos paso a paso.

Los ejercicios trabajan variables, operadores, condicionales, bucles, arrays y funciones.

1. Mostrar un mensaje

Ejercicio: mostrar el texto “Hola Mundo desde PHP”.

<?php
echo 'Hola Mundo desde PHP';
?>

2. Sumar dos números

<?php
$a = 10;
$b = 5;

$suma = $a + $b;

echo $suma;
?>

3. Calcular el promedio de tres números

<?php
$a = 8;
$b = 7;
$c = 9;

$promedio = ($a + $b + $c) / 3;

echo $promedio;
?>

4. Saber si un número es par o impar

<?php
$numero = 7;

if ($numero % 2 === 0) {
    echo 'Es par';
} else {
    echo 'Es impar';
}
?>

5. Saber si una persona es mayor de edad

<?php
$edad = 20;

if ($edad >= 18) {
    echo 'Es mayor de edad';
} else {
    echo 'Es menor de edad';
}
?>

6. Obtener el mayor de dos números

<?php
$a = 15;
$b = 22;

if ($a > $b) {
    echo $a;
} else {
    echo $b;
}
?>

7. Mostrar números del 1 al 10

<?php
for ($i = 1; $i <= 10; $i++) {
    echo $i . '<br>';
}
?>

8. Mostrar números pares del 1 al 20

<?php
for ($i = 2; $i <= 20; $i += 2) {
    echo $i . '<br>';
}
?>

9. Tabla de multiplicar

<?php
$numero = 5;

for ($i = 1; $i <= 10; $i++) {
    echo $numero . ' x ' . $i . ' = ' . ($numero * $i) . '<br>';
}
?>

10. Sumar números del 1 al 100

<?php
$suma = 0;

for ($i = 1; $i <= 100; $i++) {
    $suma += $i;
}

echo $suma;
?>

11. Contar caracteres de una palabra

<?php
$palabra = 'programacion';

echo strlen($palabra);
?>

12. Convertir texto a mayúsculas

<?php
$texto = 'hola php';

echo strtoupper($texto);
?>

13. Recorrer un array

<?php
$frutas = ['Manzana', 'Banana', 'Naranja'];

foreach ($frutas as $fruta) {
    echo $fruta . '<br>';
}
?>

14. Encontrar el mayor valor de un array

<?php
$numeros = [4, 9, 2, 15, 6];

echo max($numeros);
?>

15. Calcular el promedio de un array

<?php
$notas = [8, 7, 9, 10];

$promedio = array_sum($notas) / count($notas);

echo $promedio;
?>

16. Crear una función para saludar

<?php
function saludar(string $nombre): string
{
    return 'Hola ' . $nombre;
}

echo saludar('Ana');
?>

17. Función para calcular el área de un rectángulo

<?php
function areaRectangulo(float $base, float $altura): float
{
    return $base * $altura;
}

echo areaRectangulo(5, 3);
?>

18. Determinar si un número es positivo, negativo o cero

<?php
$numero = -3;

if ($numero > 0) {
    echo 'Positivo';
} elseif ($numero < 0) {
    echo 'Negativo';
} else {
    echo 'Cero';
}
?>

19. Contar cuántas veces aparece un valor

<?php
$numeros = [1, 2, 3, 2, 4, 2];

$resultado = array_count_values($numeros);

echo $resultado[2];
?>

20. Generar un número aleatorio

<?php
$numero = random_int(1, 100);

echo $numero;
?>

¿Qué practicar después?

Si estos ejercicios ya te resultan sencillos, el siguiente paso es trabajar con problemas intermedios que mezclen arrays, funciones, formularios, validación y acceso a bases de datos.

Serie de ejercicios de PHP

También podés ver todos los contenidos prácticos en la sección Ejercicios resueltos.

¿Te sirvió esta guía? ☕

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal

Composer en PHP: dependencias, composer.json, vendor y autoloading PSR-4

Después de aprender programación orientada a objetos y MVC, el siguiente paso para trabajar con PHP moderno es conocer Composer, el gestor de dependencias más utilizado en el ecosistema PHP.

Composer permite instalar librerías, administrar versiones y cargar clases automáticamente mediante autoloading.

Además, es una herramienta fundamental para trabajar con frameworks como Laravel.

¿Qué es Composer?

Composer es un gestor de dependencias para PHP.

Permite declarar qué librerías necesita nuestro proyecto y descargarlas automáticamente.

Por ejemplo, en lugar de copiar manualmente una librería dentro del proyecto, podemos instalarla con:

composer require monolog/monolog

Composer se encargará de descargar la librería y sus dependencias.

¿Qué problema resuelve?

Sin Composer, un proyecto podría requerir:

  • descargar librerías manualmente;
  • copiar archivos;
  • resolver dependencias;
  • verificar versiones compatibles;
  • hacer múltiples require.

Composer automatiza gran parte de ese trabajo.

Comprobar si Composer está instalado

Desde una terminal podemos ejecutar:

composer --version

Si está instalado correctamente, veremos la versión disponible.

Crear un proyecto con Composer

Dentro de una carpeta podemos ejecutar:

composer init

Composer nos hará algunas preguntas y generará un archivo llamado:

composer.json

¿Qué es composer.json?

composer.json contiene la configuración del proyecto y sus dependencias.

Un ejemplo mínimo:

{
    "name": "clubprogramador/ejemplo",
    "require": {
        "php": "^8.2"
    }
}

Instalar una dependencia

Podemos instalar una librería con:

composer require monolog/monolog

Composer modificará composer.json y descargará los paquetes necesarios.

La carpeta vendor

Composer almacena las dependencias dentro de:

vendor/

Por ejemplo:

proyecto/
│
├── app/
├── public/
├── vendor/
├── composer.json
└── composer.lock

¿Qué es composer.lock?

composer.lock registra las versiones exactas de las dependencias instaladas.

Esto permite que distintas personas o servidores instalen exactamente las mismas versiones.

Por eso, en proyectos de aplicaciones, normalmente composer.lock se incluye en el control de versiones.

composer install vs composer update

composer install

composer install

Instala las versiones indicadas en composer.lock.

composer update

composer update

Busca versiones nuevas compatibles con las reglas de composer.json y actualiza composer.lock.

Por eso no conviene ejecutar composer update indiscriminadamente en producción.

Autoloading

Composer genera automáticamente un archivo:

vendor/autoload.php

Podemos cargarlo una sola vez:

<?php

require __DIR__ . '/vendor/autoload.php';

?>

A partir de ahí, Composer puede cargar clases automáticamente.

El problema de múltiples require

Sin autoloading podríamos terminar con algo así:

require 'app/Models/Producto.php';
require 'app/Models/Usuario.php';
require 'app/Services/MailService.php';
require 'app/Controllers/ProductoController.php';

A medida que el proyecto crece, esto se vuelve difícil de mantener.

PSR-4

PSR-4 es un estándar utilizado para asociar namespaces con carpetas.

Podemos configurar Composer así:

{
    "autoload": {
        "psr-4": {
            "App\\": "app/"
        }
    }
}

Esto significa que las clases cuyo namespace comienza con App\ se buscarán dentro de la carpeta app/.

Ejemplo con namespace

Archivo:

app/Models/Producto.php

Contenido:

<?php

namespace App\Models;

class Producto
{
    public function __construct(
        public string $nombre
    ) {
    }
}

?>

Regenerar el autoload

Después de modificar la configuración de autoload podemos ejecutar:

composer dump-autoload

Usar la clase

Ahora podemos escribir:

<?php

require __DIR__ . '/vendor/autoload.php';

use App\Models\Producto;

$producto = new Producto(
    'Teclado'
);

echo $producto->nombre;

?>

No necesitamos hacer un require específico para Producto.php.

Estructura de proyecto con PSR-4

proyecto/
│
├── app/
│   ├── Controllers/
│   │   └── ProductoController.php
│   │
│   ├── Models/
│   │   └── Producto.php
│   │
│   └── Services/
│       └── ProductoService.php
│
├── public/
│   └── index.php
│
├── vendor/
├── composer.json
└── composer.lock

Namespace para un controlador

<?php

namespace App\Controllers;

use App\Models\Producto;

class ProductoController
{
    public function index(): void
    {
        // lógica del controlador
    }
}

?>

Instalar paquetes de desarrollo

Algunas dependencias solo se necesitan durante el desarrollo.

Podemos instalarlas con:

composer require --dev paquete/nombre

Estas dependencias se guardan dentro de:

require-dev

Versiones en Composer

Composer permite indicar restricciones de versiones.

Por ejemplo:

"monolog/monolog": "^3.0"

El operador ^ permite instalar actualizaciones compatibles dentro de la misma versión mayor, según las reglas de versionado semántico aplicables al paquete.

Packagist

Packagist es el repositorio público principal de paquetes utilizados por Composer.

Cuando ejecutamos:

composer require proveedor/paquete

Composer normalmente busca ese paquete en Packagist.

No subir vendor al repositorio

En muchos proyectos PHP se agrega:

/vendor/

al archivo .gitignore.

Las dependencias pueden reconstruirse ejecutando:

composer install

Lo importante es conservar:

  • composer.json;
  • composer.lock.

Ejemplo de .gitignore

/vendor/
.env

También es habitual excluir archivos con configuraciones sensibles como .env.

Composer y MVC

Podemos mejorar el proyecto MVC de la guía anterior eliminando los require manuales.

Antes:

require 'Producto.php';
require 'ProductoController.php';

Después:

require __DIR__ . '/../vendor/autoload.php';

use App\Models\Producto;
use App\Controllers\ProductoController;

Composer no es solamente para frameworks

Aunque Laravel utiliza Composer intensivamente, también es muy útil en proyectos PHP propios.

Podemos utilizar paquetes para:

  • enviar emails;
  • generar PDFs;
  • trabajar con archivos Excel;
  • manejar logs;
  • consumir APIs;
  • crear tests;
  • procesar fechas;
  • trabajar con variables de entorno.

Buenas prácticas

  • versionar composer.json y composer.lock;
  • no modificar manualmente archivos dentro de vendor;
  • utilizar autoload PSR-4;
  • mantener namespaces consistentes con la estructura de carpetas;
  • revisar las dependencias antes de instalarlas;
  • actualizar paquetes de manera controlada;
  • no ejecutar composer update en producción sin comprender el impacto.

Errores comunes

  • olvidar ejecutar composer install después de clonar un proyecto;
  • eliminar composer.lock sin motivo;
  • editar archivos dentro de vendor;
  • configurar mal el namespace PSR-4;
  • olvidar ejecutar composer dump-autoload después de ciertos cambios;
  • confundir composer install con composer update;
  • subir credenciales o secretos al repositorio.

Ejercicio práctico

Tomá el proyecto MVC de la guía anterior y adaptalo para utilizar Composer.

El objetivo es:

  1. crear un archivo composer.json;
  2. configurar App\ con PSR-4;
  3. agregar namespaces a Modelos y Controladores;
  4. eliminar los require individuales;
  5. cargar solamente vendor/autoload.php;
  6. ejecutar composer dump-autoload;
  7. comprobar que el CRUD siga funcionando.

¿Qué sigue?

Con POO, MVC y Composer ya tenemos gran parte de la base conceptual necesaria para comprender un framework PHP moderno.

La próxima guía puede ser una introducción a Laravel desde cero:

  • qué es Laravel;
  • cómo instalarlo;
  • estructura del proyecto;
  • rutas;
  • controladores;
  • Blade;
  • modelos;
  • migraciones.

¿Te sirvió esta guía? ☕

Si este contenido te ayudó y querés apoyar a Club Programador para seguir publicando ejercicios, proyectos y guías gratuitas, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal

MVC en PHP desde cero: Modelo, Vista y Controlador con un ejemplo completo

Hasta ahora construimos formularios, consultas con PDO, CRUD, autenticación, permisos, seguridad y programación orientada a objetos. El siguiente paso es aprender a organizar mejor una aplicación PHP.

Una de las arquitecturas más conocidas para separar responsabilidades es MVC: Modelo, Vista y Controlador.

En esta guía vamos a entender qué hace cada parte y construiremos un ejemplo sencillo con productos y MySQL.

¿Qué problema resuelve MVC?

En proyectos pequeños es común comenzar con archivos que mezclan:

  • HTML;
  • consultas SQL;
  • validaciones;
  • lógica de negocio;
  • redirecciones;
  • procesamiento de formularios.

Por ejemplo, un único archivo podría consultar productos, procesar un POST, validar datos y generar todo el HTML.

Eso puede funcionar al principio, pero a medida que la aplicación crece se vuelve difícil de mantener.

MVC intenta separar esas responsabilidades.

¿Qué significa MVC?

MVC significa:

  • Model: Modelo;
  • View: Vista;
  • Controller: Controlador.

Modelo

El Modelo se ocupa de trabajar con los datos y con la lógica relacionada con ellos.

Por ejemplo:

  • consultar productos;
  • buscar un producto por ID;
  • crear registros;
  • actualizarlos;
  • eliminarlos.

Vista

La Vista se ocupa de presentar la información al usuario.

Normalmente contiene:

  • HTML;
  • tablas;
  • formularios;
  • mensajes;
  • pequeñas expresiones PHP para mostrar datos.

La vista no debería contener consultas SQL.

Controlador

El Controlador coordina la aplicación.

Por ejemplo:

  1. recibe una petición;
  2. solicita información al modelo;
  3. procesa la lógica necesaria;
  4. envía datos a la vista.

Flujo básico de MVC

Usuario
   ↓
Controlador
   ↓
Modelo
   ↓
Base de datos
   ↓
Controlador
   ↓
Vista
   ↓
Usuario

No todas las aplicaciones MVC implementan exactamente el mismo flujo, pero esta idea ayuda a entender la separación de responsabilidades.

Estructura de carpetas

Podemos comenzar con una estructura simple:

proyecto/
│
├── app/
│   ├── Controllers/
│   │   └── ProductoController.php
│   │
│   ├── Models/
│   │   └── Producto.php
│   │
│   └── Views/
│       └── productos/
│           ├── index.php
│           └── create.php
│
├── config/
│   └── database.php
│
└── public/
    └── index.php

Base de datos de ejemplo

Vamos a utilizar una tabla productos:

CREATE TABLE productos (
    id INT AUTO_INCREMENT PRIMARY KEY,
    nombre VARCHAR(100) NOT NULL,
    precio DECIMAL(10,2) NOT NULL,
    stock INT NOT NULL DEFAULT 0
);

Conexión a MySQL

En config/database.php podemos centralizar la conexión:

<?php

$host = 'localhost';
$db = 'tienda';
$user = 'root';
$password = '';

$dsn = "mysql:host=$host;dbname=$db;charset=utf8mb4";

$pdo = new PDO($dsn, $user, $password, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]);

?>

Crear el Modelo Producto

El modelo será responsable de consultar la tabla.

<?php

class Producto
{
    public function __construct(
        private PDO $pdo
    ) {
    }

    public function todos(): array
    {
        $stmt = $this->pdo->query(
            'SELECT id, nombre, precio, stock
             FROM productos
             ORDER BY id DESC'
        );

        return $stmt->fetchAll();
    }
}

?>

El modelo conoce PDO y la base de datos. La vista no necesita saber cómo se obtuvieron los productos.

Crear el Controlador

En app/Controllers/ProductoController.php:

<?php

class ProductoController
{
    public function __construct(
        private Producto $producto
    ) {
    }

    public function index(): void
    {
        $productos = $this->producto->todos();

        require __DIR__
            . '/../Views/productos/index.php';
    }
}

?>

El controlador solicita los datos al modelo y carga la vista.

Crear la Vista

En app/Views/productos/index.php:

<h1>Productos</h1>

<table>
    <thead>
        <tr>
            <th>ID</th>
            <th>Nombre</th>
            <th>Precio</th>
            <th>Stock</th>
        </tr>
    </thead>

    <tbody>

    <?php foreach ($productos as $producto): ?>

        <tr>
            <td>
                <?= $producto['id'] ?>
            </td>

            <td>
                <?= htmlspecialchars(
                    $producto['nombre'],
                    ENT_QUOTES,
                    'UTF-8'
                ) ?>
            </td>

            <td>
                $<?= number_format(
                    $producto['precio'],
                    2,
                    ',',
                    '.'
                ) ?>
            </td>

            <td>
                <?= $producto['stock'] ?>
            </td>
        </tr>

    <?php endforeach; ?>

    </tbody>
</table>

Crear un punto de entrada

Podemos utilizar public/index.php como punto de entrada:

<?php

require __DIR__ . '/../config/database.php';
require __DIR__ . '/../app/Models/Producto.php';
require __DIR__ . '/../app/Controllers/ProductoController.php';

$modelo = new Producto($pdo);

$controller = new ProductoController($modelo);

$controller->index();

?>

¿Qué logramos?

Ahora cada responsabilidad está separada:

  • Producto.php trabaja con los datos;
  • ProductoController.php coordina la petición;
  • index.php muestra el HTML.

Agregar un formulario de creación

Podemos agregar un método al controlador:

public function create(): void
{
    require __DIR__
        . '/../Views/productos/create.php';
}

Vista para crear un producto

<h1>Nuevo producto</h1>

<form method="POST" action="?accion=store">

    <label>Nombre</label>
    <input type="text" name="nombre" required>

    <label>Precio</label>
    <input type="number" step="0.01" name="precio" required>

    <label>Stock</label>
    <input type="number" name="stock" required>

    <button type="submit">
        Guardar
    </button>

</form>

Agregar el método crear() al Modelo

public function crear(
    string $nombre,
    float $precio,
    int $stock
): bool {
    $sql = 'INSERT INTO productos
            (nombre, precio, stock)
            VALUES (:nombre, :precio, :stock)';

    $stmt = $this->pdo->prepare($sql);

    return $stmt->execute([
        'nombre' => $nombre,
        'precio' => $precio,
        'stock' => $stock
    ]);
}

Agregar store() al Controlador

public function store(): void
{
    $nombre = trim($_POST['nombre'] ?? '');
    $precio = filter_input(
        INPUT_POST,
        'precio',
        FILTER_VALIDATE_FLOAT
    );

    $stock = filter_input(
        INPUT_POST,
        'stock',
        FILTER_VALIDATE_INT
    );

    if (
        $nombre === '' ||
        $precio === false ||
        $stock === false
    ) {
        exit('Datos inválidos');
    }

    $this->producto->crear(
        $nombre,
        $precio,
        $stock
    );

    header('Location: index.php');
    exit;
}

Agregar un router simple

Podemos decidir qué método ejecutar mediante un parámetro:

$accion = $_GET['accion'] ?? 'index';

switch ($accion) {

    case 'create':
        $controller->create();
        break;

    case 'store':
        $controller->store();
        break;

    default:
        $controller->index();
}

Esto todavía es un router muy básico, pero sirve para entender la idea.

Modelo no significa necesariamente tabla

Es común pensar que cada modelo debe representar una tabla exactamente, pero el concepto es más amplio.

El modelo representa los datos y reglas de negocio de la aplicación.

En proyectos simples puede coincidir bastante con una tabla, pero en sistemas mayores puede incluir lógica más compleja.

La Vista no debería consultar la base

Evitemos algo como esto dentro de una vista:

// No recomendado dentro de una vista

$stmt = $pdo->query(
    'SELECT * FROM productos'
);

La vista debería recibir los datos ya preparados.

El Controlador tampoco debería hacer todo

Otro error frecuente es mover todo el código del archivo original hacia el controlador.

Un controlador gigantesco sigue siendo difícil de mantener.

La idea es que coordine responsabilidades, no que concentre toda la aplicación.

Separar la lógica de negocio

A medida que el proyecto crece, podemos tener otras capas o servicios.

Por ejemplo:

app/
├── Controllers/
├── Models/
├── Services/
└── Views/

Un servicio podría encargarse de reglas específicas que no pertenecen directamente a una vista o a una consulta SQL.

MVC y seguridad

La arquitectura no reemplaza las buenas prácticas vistas anteriormente.

Seguimos necesitando:

  • consultas preparadas;
  • validación de datos;
  • protección CSRF;
  • escape de salida;
  • autenticación;
  • autorización.

MVC no es una regla rígida

Existen diferentes formas de implementar MVC.

Dos frameworks pueden utilizar el patrón y tener estructuras distintas.

Lo importante para comenzar es comprender la separación de responsabilidades.

¿Cómo se relaciona esto con Laravel?

Laravel utiliza muchos de estos conceptos.

Por ejemplo:

  • los Models representan y trabajan con datos;
  • los Controllers reciben peticiones y coordinan acciones;
  • las vistas se construyen normalmente con Blade;
  • las rutas determinan qué controlador debe ejecutarse.

Laravel agrega muchas herramientas alrededor de esta arquitectura, pero entender MVC primero hace que sea mucho más fácil comprender el framework.

Errores comunes al aprender MVC

  • poner consultas SQL dentro de las vistas;
  • crear controladores que hacen absolutamente todo;
  • mezclar HTML dentro de los modelos;
  • crear una clase por cada pequeño fragmento de código sin necesidad;
  • pensar que MVC obliga a usar una estructura exacta;
  • creer que usar carpetas llamadas Models, Views y Controllers ya convierte automáticamente un proyecto en MVC.

Ejercicio práctico

Extendé el ejemplo de productos para implementar un CRUD completo utilizando MVC.

Debería incluir:

  1. listar productos;
  2. crear productos;
  3. editar productos;
  4. eliminar productos;
  5. validar formularios;
  6. usar consultas preparadas;
  7. proteger la eliminación mediante POST;
  8. separar correctamente Modelo, Vista y Controlador.

¿Qué sigue?

En la próxima guía vamos a aprender Composer y autoloading en PHP.

Veremos:

  • qué es Composer;
  • qué es composer.json;
  • cómo instalar paquetes;
  • qué es vendor;
  • autoloading;
  • PSR-4;
  • cómo dejar de utilizar múltiples require manuales.

Ese paso nos acercará todavía más al funcionamiento interno de proyectos PHP modernos y frameworks como Laravel.

¿Te sirvió esta guía? ☕

Si este contenido te ayudó y querés apoyar a Club Programador para seguir publicando ejercicios, proyectos y guías gratuitas, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal

Programación orientada a objetos en PHP: clases, objetos, herencia e interfaces

Hasta ahora trabajamos con PHP utilizando variables, funciones, formularios, PDO, sesiones, autenticación y seguridad. El siguiente paso es aprender programación orientada a objetos, una forma de organizar el código en clases y objetos.

Este enfoque es fundamental para trabajar en proyectos medianos o grandes y es la base de muchos frameworks modernos, incluido Laravel.

¿Qué es la programación orientada a objetos?

La programación orientada a objetos, también llamada POO u OOP, organiza el código alrededor de objetos que representan entidades del sistema.

Por ejemplo, en una aplicación podemos tener objetos como:

  • Usuario;
  • Producto;
  • Pedido;
  • Curso;
  • Factura.

Cada objeto puede tener datos y comportamientos propios.

¿Qué es una clase?

Una clase funciona como una plantilla para crear objetos.

<?php

class Usuario
{
}

?>

Crear un objeto

Para crear una instancia utilizamos new:

$usuario = new Usuario();

$usuario es ahora un objeto de la clase Usuario.

Propiedades

Las propiedades representan los datos del objeto.

class Usuario
{
    public string $nombre;
    public string $email;
}

Podemos asignar valores:

$usuario = new Usuario();

$usuario->nombre = 'Ana';
$usuario->email = 'ana@email.com';

Métodos

Los métodos representan comportamientos del objeto.

class Usuario
{
    public string $nombre;

    public function saludar(): string
    {
        return 'Hola, soy ' . $this->nombre;
    }
}

Para llamarlo:

$usuario = new Usuario();
$usuario->nombre = 'Ana';

echo $usuario->saludar();

¿Qué significa $this?

$this hace referencia al objeto actual.

Por ejemplo:

$this->nombre

significa “la propiedad nombre de este objeto”.

El constructor

El constructor se ejecuta automáticamente al crear un objeto.

class Usuario
{
    public string $nombre;
    public string $email;

    public function __construct(string $nombre, string $email)
    {
        $this->nombre = $nombre;
        $this->email = $email;
    }
}

Ahora podemos crear el objeto directamente con sus datos:

$usuario = new Usuario(
    'Ana',
    'ana@email.com'
);

Promoción de propiedades en el constructor

En versiones modernas de PHP podemos simplificar el código:

class Usuario
{
    public function __construct(
        public string $nombre,
        public string $email
    ) {
    }
}

Visibilidad: public, protected y private

Las propiedades y métodos pueden tener distintos niveles de acceso.

public

Puede utilizarse desde cualquier parte:

public string $nombre;

private

Solo puede utilizarse dentro de la misma clase:

private string $password;

protected

Puede utilizarse dentro de la clase y sus clases hijas:

protected string $rol;

Encapsulamiento

El encapsulamiento consiste en proteger el estado interno de un objeto y controlar cómo se accede a él.

class Cuenta
{
    private float $saldo = 0;

    public function depositar(float $importe): void
    {
        if ($importe <= 0) {
            return;
        }

        $this->saldo += $importe;
    }

    public function obtenerSaldo(): float
    {
        return $this->saldo;
    }
}

El saldo no puede modificarse directamente desde afuera.

Getters y setters

Los getters permiten leer valores y los setters modificarlos de forma controlada.

class Producto
{
    private float $precio;

    public function setPrecio(float $precio): void
    {
        if ($precio < 0) {
            throw new InvalidArgumentException(
                'El precio no puede ser negativo'
            );
        }

        $this->precio = $precio;
    }

    public function getPrecio(): float
    {
        return $this->precio;
    }
}

Herencia

Una clase puede heredar propiedades y métodos de otra utilizando extends.

class Usuario
{
    public function __construct(
        protected string $nombre
    ) {
    }

    public function getNombre(): string
    {
        return $this->nombre;
    }
}

class Administrador extends Usuario
{
    public function eliminarUsuario(): string
    {
        return 'Usuario eliminado';
    }
}

Sobrescribir métodos

Una clase hija puede redefinir un método de la clase padre.

class Usuario
{
    public function descripcion(): string
    {
        return 'Usuario del sistema';
    }
}

class Administrador extends Usuario
{
    public function descripcion(): string
    {
        return 'Administrador del sistema';
    }
}

parent::

Podemos llamar a un método de la clase padre utilizando parent::.

class Administrador extends Usuario
{
    public function descripcion(): string
    {
        return parent::descripcion()
            . ' con permisos administrativos';
    }
}

Clases abstractas

Una clase abstracta sirve como base para otras clases y no puede instanciarse directamente.

abstract class Empleado
{
    public function __construct(
        protected string $nombre
    ) {
    }

    abstract public function calcularSueldo(): float;
}

Una clase hija debe implementar el método abstracto:

class EmpleadoMensual extends Empleado
{
    public function __construct(
        string $nombre,
        private float $sueldo
    ) {
        parent::__construct($nombre);
    }

    public function calcularSueldo(): float
    {
        return $this->sueldo;
    }
}

Interfaces

Una interfaz define qué métodos debe implementar una clase.

interface Notificable
{
    public function enviar(string $mensaje): void;
}

Después podemos crear distintas implementaciones:

class NotificacionEmail implements Notificable
{
    public function enviar(string $mensaje): void
    {
        echo 'Enviando email: ' . $mensaje;
    }
}

Polimorfismo

Gracias a una interfaz podemos trabajar con diferentes implementaciones utilizando el mismo contrato.

function notificar(
    Notificable $servicio,
    string $mensaje
): void {
    $servicio->enviar($mensaje);
}

La función no necesita saber cómo se envía realmente la notificación.

Propiedades y métodos static

Un miembro static pertenece a la clase y no a una instancia concreta.

class Conversor
{
    public static function kilometrosAMillas(float $km): float
    {
        return $km * 0.621371;
    }
}

Se utiliza así:

$millas = Conversor::kilometrosAMillas(10);

Constantes de clase

class Pedido
{
    public const ESTADO_PENDIENTE = 'pendiente';
    public const ESTADO_PAGADO = 'pagado';
}

Podemos acceder mediante:

Pedido::ESTADO_PAGADO

Namespaces

Cuando un proyecto crece, los namespaces ayudan a organizar clases y evitar conflictos de nombres.

<?php

namespace App\Models;

class Usuario
{
}

?>

Luego podemos importar la clase:

use App\Models\Usuario;

$usuario = new Usuario();

Ejemplo práctico: clase Producto

class Producto
{
    public function __construct(
        private int $id,
        private string $nombre,
        private float $precio,
        private int $stock
    ) {
    }

    public function getNombre(): string
    {
        return $this->nombre;
    }

    public function getPrecio(): float
    {
        return $this->precio;
    }

    public function tieneStock(): bool
    {
        return $this->stock > 0;
    }

    public function descontarStock(int $cantidad): void
    {
        if ($cantidad <= 0 || $cantidad > $this->stock) {
            throw new InvalidArgumentException(
                'Cantidad inválida'
            );
        }

        $this->stock -= $cantidad;
    }
}

Ventajas de la programación orientada a objetos

  • organiza mejor proyectos grandes;
  • permite reutilizar código;
  • facilita separar responsabilidades;
  • mejora el mantenimiento;
  • permite crear componentes reemplazables;
  • facilita las pruebas automatizadas;
  • es la base conceptual de frameworks como Laravel.

No todo necesita ser una clase

La POO es una herramienta, no una obligación.

Crear clases sin una responsabilidad clara puede hacer que el código sea más complejo en lugar de mejorarlo.

Una buena clase debería representar una responsabilidad concreta y mantener relacionados sus datos y comportamientos.

Errores comunes

  • hacer todas las propiedades públicas;
  • crear clases que solamente agrupan funciones sin una responsabilidad clara;
  • abusar de la herencia;
  • crear jerarquías demasiado complejas;
  • utilizar métodos static para todo;
  • confundir una clase con un objeto;
  • no utilizar tipos de datos cuando ayudan a detectar errores;
  • crear clases gigantes que hacen demasiadas cosas.

Ejercicio práctico

Creá un pequeño sistema de biblioteca utilizando POO.

Definí:

  • una clase Libro con título, autor y disponibilidad;
  • una clase Usuario;
  • un método para prestar un libro;
  • un método para devolverlo;
  • encapsulamiento para impedir cambios inválidos;
  • una interfaz Notificable para informar cuando se realiza un préstamo.

¿Qué sigue?

En la próxima guía vamos a organizar un proyecto PHP utilizando una arquitectura más clara: MVC (Modelo, Vista y Controlador).

Veremos cómo separar:

  • acceso a datos;
  • lógica de negocio;
  • controladores;
  • vistas;
  • rutas.

Esto nos va a acercar mucho más a la estructura que utilizan frameworks como Laravel.

¿Te sirvió esta guía? ☕

Si este contenido te ayudó y querés apoyar a Club Programador para seguir publicando ejercicios, proyectos y guías gratuitas, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal

Seguridad en PHP: prevenir SQL Injection, XSS, CSRF y validar datos correctamente

Después de implementar login, sesiones, roles y permisos, el siguiente paso es aprender a proteger correctamente una aplicación PHP.

La seguridad no consiste en una sola técnica. Una aplicación puede utilizar contraseñas seguras y, al mismo tiempo, ser vulnerable a inyección SQL, XSS o CSRF.

En esta guía vamos a repasar algunas de las vulnerabilidades más comunes y cómo prevenirlas.

1. SQL Injection

Una inyección SQL ocurre cuando datos ingresados por el usuario terminan formando parte directa de una consulta SQL.

Este código es inseguro:

<?php

$email = $_POST['email'];

$sql = "SELECT * FROM usuarios WHERE email = '$email'";

$usuario = $pdo->query($sql)->fetch();

?>

El problema es que el contenido de $email se concatena directamente dentro de la consulta.

La solución: consultas preparadas

Con PDO debemos separar los datos de la consulta:

<?php

$sql = 'SELECT * FROM usuarios WHERE email = :email';

$stmt = $pdo->prepare($sql);

$stmt->execute([
    'email' => $email
]);

$usuario = $stmt->fetch(PDO::FETCH_ASSOC);

?>

El marcador :email evita que el valor recibido sea interpretado como parte de la consulta SQL.

No confiar en addslashes()

Escapar manualmente textos no reemplaza a las consultas preparadas.

La opción recomendada es utilizar prepare() y execute().

2. XSS: Cross-Site Scripting

XSS aparece cuando mostramos contenido enviado por un usuario sin escapar correctamente el HTML.

Por ejemplo:

echo $_POST['nombre'];

Si el usuario envía código HTML o JavaScript, ese contenido podría ejecutarse en el navegador.

Escapar la salida con htmlspecialchars()

echo htmlspecialchars(
    $_POST['nombre'],
    ENT_QUOTES,
    'UTF-8'
);

Esto convierte caracteres especiales en entidades HTML y evita que el navegador los interprete como etiquetas.

Escapar al mostrar, no necesariamente al guardar

Una regla práctica importante es guardar los datos de forma adecuada y aplicar el escape según el contexto de salida.

Por ejemplo:

<h1>
    <?= htmlspecialchars($usuario['nombre'], ENT_QUOTES, 'UTF-8') ?>
</h1>

3. Validación y sanitización

Validar significa comprobar que un dato cumple las reglas esperadas.

Sanitizar significa limpiar o transformar un valor.

Validar un email

if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
    $errores[] = 'El email no es válido';
}

Validar un número entero

$edad = filter_input(INPUT_POST, 'edad', FILTER_VALIDATE_INT);

if ($edad === false) {
    $errores[] = 'La edad debe ser un número entero';
}

No confiar solamente en validaciones HTML

Este campo:

<input type="email" name="email" required>

mejora la experiencia del usuario, pero no reemplaza la validación en PHP.

Las peticiones pueden enviarse sin utilizar el formulario del navegador.

4. CSRF: Cross-Site Request Forgery

Un ataque CSRF intenta lograr que un usuario autenticado ejecute una acción sin darse cuenta.

Por ejemplo, una petición podría intentar eliminar un usuario utilizando la sesión activa de un administrador.

Crear un token CSRF

Podemos generar un valor aleatorio y guardarlo en la sesión:

<?php

session_start();

if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

?>

Agregar el token al formulario

<input
    type="hidden"
    name="csrf_token"
    value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>"
>

Validar el token

if (
    !isset($_POST['csrf_token']) ||
    !hash_equals(
        $_SESSION['csrf_token'],
        $_POST['csrf_token']
    )
) {
    http_response_code(403);
    exit('Token CSRF inválido');
}

Las operaciones importantes deberían usar POST

No conviene eliminar datos utilizando enlaces como:

<a href="eliminar.php?id=10">Eliminar</a>

Para operaciones que modifican información es preferible utilizar formularios con método POST y protección CSRF.

5. Contraseñas seguras

Las contraseñas nunca deben almacenarse en texto plano.

Para guardarlas:

$hash = password_hash(
    $password,
    PASSWORD_DEFAULT
);

Para comprobarlas:

if (password_verify($password, $hash)) {
    // Contraseña correcta
}

No utilizar MD5 o SHA1 para contraseñas

Aunque son funciones hash, md5() y sha1() no están diseñadas para almacenar contraseñas de forma segura.

PHP ofrece específicamente:

  • password_hash();
  • password_verify();
  • password_needs_rehash().

6. Seguridad de las sesiones

Después de un login correcto conviene regenerar el identificador:

session_regenerate_id(true);

También podemos configurar las cookies de sesión:

session_set_cookie_params([
    'httponly' => true,
    'secure' => true,
    'samesite' => 'Lax'
]);

session_start();

secure debe utilizarse cuando el sitio funciona mediante HTTPS.

7. No mostrar errores sensibles en producción

Durante el desarrollo puede ser útil visualizar errores, pero en producción pueden revelar información interna.

Por ejemplo:

  • rutas del servidor;
  • nombres de archivos;
  • estructura de la base de datos;
  • credenciales mal configuradas;
  • detalles del código.

En producción conviene registrar errores en logs y mostrar al usuario mensajes genéricos.

8. Manejo seguro de archivos subidos

Las subidas de archivos requieren controles adicionales.

No debemos confiar solamente en:

$_FILES['archivo']['type']

Ese valor proviene del cliente.

Podemos analizar el tipo MIME real con finfo:

$finfo = new finfo(FILEINFO_MIME_TYPE);

$tipo = $finfo->file(
    $_FILES['archivo']['tmp_name']
);

Después podemos compararlo contra una lista permitida.

$permitidos = [
    'image/jpeg',
    'image/png'
];

if (!in_array($tipo, $permitidos, true)) {
    exit('Tipo de archivo no permitido');
}

No utilizar directamente el nombre original

En vez de confiar en el nombre recibido, podemos generar uno aleatorio:

$nombreSeguro = bin2hex(random_bytes(16)) . '.jpg';

9. Validar identificadores

Un identificador recibido mediante GET también debe validarse.

$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);

if (!$id) {
    http_response_code(400);
    exit('ID inválido');
}

10. Autorización en cada operación

Aunque un usuario conozca una URL, no debería poder ejecutar una operación sin permiso.

Por ejemplo:

if (!tienePermiso(
    $pdo,
    $_SESSION['usuario_id'],
    'usuarios.eliminar'
)) {
    http_response_code(403);
    exit('Acceso denegado');
}

11. Variables de entorno y credenciales

No conviene publicar credenciales directamente en archivos accesibles o repositorios públicos.

Información como:

  • usuario de MySQL;
  • contraseña de la base;
  • tokens de APIs;
  • claves privadas;

debe tratarse como información sensible.

12. Aplicar el principio de mínimo privilegio

Un usuario de base de datos utilizado por una aplicación no necesariamente necesita permisos administrativos completos.

La idea es conceder solamente los privilegios necesarios para que la aplicación funcione.

Ejemplo de formulario más seguro

<?php

session_start();

if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

?>

<form method="POST">

    <input
        type="hidden"
        name="csrf_token"
        value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>"
    >

    <label>Email</label>

    <input
        type="email"
        name="email"
        required
    >

    <button type="submit">
        Guardar
    </button>

</form>

Procesar el formulario

<?php

if ($_SERVER['REQUEST_METHOD'] === 'POST') {

    if (
        !isset($_POST['csrf_token']) ||
        !hash_equals(
            $_SESSION['csrf_token'],
            $_POST['csrf_token']
        )
    ) {
        http_response_code(403);
        exit('Solicitud inválida');
    }

    $email = filter_input(
        INPUT_POST,
        'email',
        FILTER_VALIDATE_EMAIL
    );

    if (!$email) {
        exit('Email inválido');
    }

    $stmt = $pdo->prepare(
        'UPDATE usuarios
         SET email = :email
         WHERE id = :id'
    );

    $stmt->execute([
        'email' => $email,
        'id' => $_SESSION['usuario_id']
    ]);
}

?>

Checklist de seguridad para una aplicación PHP

  • usar PDO con consultas preparadas;
  • validar todos los datos externos;
  • escapar correctamente el contenido al mostrarlo;
  • proteger formularios sensibles contra CSRF;
  • usar password_hash() y password_verify();
  • regenerar la sesión después del login;
  • verificar roles y permisos en el servidor;
  • no confiar en cookies ni campos ocultos;
  • validar correctamente archivos subidos;
  • no mostrar errores internos en producción;
  • utilizar HTTPS;
  • mantener PHP y las dependencias actualizadas.

Errores comunes

  • creer que la validación de JavaScript es suficiente;
  • concatenar valores dentro de consultas SQL;
  • mostrar contenido de usuarios sin escapar;
  • usar GET para acciones destructivas;
  • guardar contraseñas con MD5 o SHA1;
  • confiar en el nombre o tipo MIME enviado por un archivo;
  • ocultar botones sin proteger la operación real;
  • guardar secretos directamente en repositorios públicos.

Ejercicio práctico

Tomá el sistema de usuarios de las guías anteriores y agregale:

  1. consultas preparadas en todas las operaciones;
  2. escape de nombres y datos mostrados en HTML;
  3. validación del email;
  4. token CSRF para editar y eliminar usuarios;
  5. protección por permisos;
  6. regeneración de sesión al iniciar sesión;
  7. mensajes de error seguros.

¿Qué sigue?

En la próxima guía podemos avanzar con programación orientada a objetos en PHP, un paso fundamental antes de comenzar a trabajar con arquitecturas más organizadas y frameworks como Laravel.

Veremos clases, objetos, propiedades, métodos, constructores, encapsulamiento, herencia e interfaces.

¿Te sirvió esta guía? ☕

Si este contenido te ayudó y querés apoyar a Club Programador para seguir publicando ejercicios, proyectos y guías gratuitas, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal

Autenticación y autorización en PHP: roles, permisos y protección de páginas

En la guía anterior vimos cómo implementar un sistema básico de login y logout con sesiones, cookies, PDO y contraseñas seguras. Ahora vamos a dar el siguiente paso: controlar qué puede hacer cada usuario.

Para eso necesitamos distinguir dos conceptos:

  • autenticación: comprobar quién es el usuario;
  • autorización: decidir qué acciones puede realizar.

Autenticación vs autorización

Cuando un usuario inicia sesión correctamente, queda autenticado.

Pero eso no significa que pueda acceder a cualquier parte del sistema.

Por ejemplo:

  • un usuario común puede ver su perfil;
  • un administrador puede gestionar usuarios;
  • un moderador puede editar contenidos;
  • un invitado no puede acceder a páginas privadas.

Agregar un rol a la tabla de usuarios

Una forma simple de comenzar es agregar una columna rol.

ALTER TABLE usuarios
ADD rol VARCHAR(50) NOT NULL DEFAULT 'usuario';

Por ejemplo, podemos tener:

  • usuario;
  • admin;
  • moderador.

Guardar el rol en la sesión

Después de validar el login, podemos guardar el rol del usuario:

$_SESSION['usuario_id'] = $usuario['id'];
$_SESSION['usuario_nombre'] = $usuario['nombre'];
$_SESSION['usuario_rol'] = $usuario['rol'];

Proteger una página para usuarios autenticados

<?php

session_start();

if (!isset($_SESSION['usuario_id'])) {
    header('Location: login.php');
    exit;
}

?>

Proteger una página solo para administradores

<?php

session_start();

if (
    !isset($_SESSION['usuario_id']) ||
    $_SESSION['usuario_rol'] !== 'admin'
) {
    http_response_code(403);
    exit('Acceso denegado');
}

?>

No alcanza con ocultar botones

Un error muy común es esconder botones en la interfaz y pensar que eso protege la acción.

Por ejemplo:

<?php if ($_SESSION['usuario_rol'] === 'admin'): ?>

    <a href="eliminar_usuario.php?id=15">
        Eliminar usuario
    </a>

<?php endif; ?>

Esto mejora la interfaz, pero no reemplaza el control en el servidor.

La página eliminar_usuario.php también debe verificar que el usuario tenga permisos.

Centralizar la verificación

En lugar de repetir las mismas condiciones en todas las páginas, podemos crear funciones.

<?php

function usuarioAutenticado(): bool
{
    return isset($_SESSION['usuario_id']);
}

function esAdmin(): bool
{
    return isset($_SESSION['usuario_rol'])
        && $_SESSION['usuario_rol'] === 'admin';
}

?>

Crear funciones para exigir permisos

function requireLogin(): void
{
    if (!isset($_SESSION['usuario_id'])) {
        header('Location: login.php');
        exit;
    }
}

function requireAdmin(): void
{
    requireLogin();

    if ($_SESSION['usuario_rol'] !== 'admin') {
        http_response_code(403);
        exit('No tenés permisos para acceder a esta sección');
    }
}

Después podemos utilizar:

<?php

session_start();

require 'auth.php';

requireAdmin();

?>

Roles en una aplicación real

Para proyectos pequeños, guardar el rol directamente en la tabla usuarios puede ser suficiente.

Sin embargo, cuando el sistema crece suele ser mejor separar la información.

Por ejemplo:

CREATE TABLE roles (
    id INT AUTO_INCREMENT PRIMARY KEY,
    nombre VARCHAR(50) NOT NULL UNIQUE
);
ALTER TABLE usuarios
ADD rol_id INT,
ADD FOREIGN KEY (rol_id)
    REFERENCES roles(id);

¿Qué son los permisos?

Un rol es una agrupación de capacidades.

Por ejemplo, el rol admin podría tener estos permisos:

  • ver usuarios;
  • crear usuarios;
  • editar usuarios;
  • eliminar usuarios.

Mientras que el rol usuario podría tener solamente:

  • ver su perfil;
  • editar su perfil.

Modelo con roles y permisos

Una estructura más flexible podría utilizar:

  • usuarios;
  • roles;
  • permisos;
  • rol_permiso.

Esto permite asignar muchos permisos a cada rol.

Tabla de permisos

CREATE TABLE permisos (
    id INT AUTO_INCREMENT PRIMARY KEY,
    nombre VARCHAR(100) NOT NULL UNIQUE
);

Tabla intermedia rol_permiso

CREATE TABLE rol_permiso (
    rol_id INT NOT NULL,
    permiso_id INT NOT NULL,

    PRIMARY KEY (rol_id, permiso_id),

    FOREIGN KEY (rol_id)
        REFERENCES roles(id),

    FOREIGN KEY (permiso_id)
        REFERENCES permisos(id)
);

Consultar los permisos de un usuario

SELECT p.nombre
FROM usuarios u
INNER JOIN roles r
    ON u.rol_id = r.id
INNER JOIN rol_permiso rp
    ON r.id = rp.rol_id
INNER JOIN permisos p
    ON rp.permiso_id = p.id
WHERE u.id = :usuario_id;

Función tienePermiso()

Una implementación simple podría ser:

function tienePermiso(PDO $pdo, int $usuarioId, string $permiso): bool
{
    $sql = 'SELECT COUNT(*)
            FROM usuarios u
            INNER JOIN roles r
                ON u.rol_id = r.id
            INNER JOIN rol_permiso rp
                ON r.id = rp.rol_id
            INNER JOIN permisos p
                ON rp.permiso_id = p.id
            WHERE u.id = :usuario_id
            AND p.nombre = :permiso';

    $stmt = $pdo->prepare($sql);

    $stmt->execute([
        'usuario_id' => $usuarioId,
        'permiso' => $permiso
    ]);

    return (bool) $stmt->fetchColumn();
}

Usar permisos para proteger acciones

if (!tienePermiso(
    $pdo,
    $_SESSION['usuario_id'],
    'usuarios.eliminar'
)) {
    http_response_code(403);
    exit('Acceso denegado');
}

Controlar acciones, no solo páginas

Los permisos deben aplicarse también sobre operaciones específicas.

Por ejemplo, aunque un usuario pueda acceder a una lista, eso no significa necesariamente que pueda:

  • crear registros;
  • editarlos;
  • eliminarlos;
  • exportarlos.

HTTP 401 y 403

Son dos códigos que suelen confundirse.

  • 401 Unauthorized: normalmente indica que falta autenticación válida.
  • 403 Forbidden: el usuario está identificado, pero no tiene permiso para realizar la acción.

No confiar en datos enviados por el cliente

Nunca debemos decidir permisos basándonos en un campo enviado desde un formulario.

Esto sería inseguro:

<input type="hidden" name="rol" value="admin">

El usuario puede modificar fácilmente ese valor.

El rol y los permisos deben obtenerse desde una fuente confiable, como la sesión y la base de datos.

Actualizar permisos durante una sesión

Si guardamos el rol únicamente en $_SESSION y luego un administrador cambia ese rol en la base de datos, la sesión podría conservar temporalmente el valor anterior.

En sistemas donde los permisos cambian con frecuencia puede ser conveniente consultar los permisos desde la base de datos o implementar una estrategia para actualizar la sesión.

Ejemplo de estructura de archivos

proyecto/
│
├── config/
│   └── conexion.php
│
├── auth/
│   └── auth.php
│
├── admin/
│   ├── index.php
│   └── usuarios.php
│
├── usuario/
│   └── perfil.php
│
├── login.php
├── logout.php
└── index.php

Esto ayuda a separar responsabilidades y mantener el proyecto ordenado.

Errores comunes

  • confundir autenticación con autorización;
  • proteger solamente los botones y no las acciones del servidor;
  • repetir controles de permisos por todo el proyecto;
  • confiar en roles enviados mediante formularios o parámetros;
  • dar permisos excesivos a usuarios comunes;
  • guardar demasiada información sensible dentro de la sesión;
  • no devolver una respuesta adecuada cuando falta autorización.

Ejercicio práctico

Extendé el sistema de login de la guía anterior para tener dos roles:

  • admin;
  • usuario.

Después implementá:

  1. una página accesible por cualquier usuario autenticado;
  2. una página disponible solamente para administradores;
  3. un menú que muestre opciones diferentes según el rol;
  4. protección en el servidor para las operaciones administrativas;
  5. una respuesta HTTP 403 cuando un usuario no tenga permisos.

¿Qué sigue?

En la próxima guía podemos avanzar con un tema fundamental para cualquier aplicación web: seguridad en PHP.

Veremos:

  • inyección SQL;
  • XSS;
  • CSRF;
  • validación y sanitización;
  • protección de formularios;
  • manejo seguro de contraseñas y sesiones.

¿Te sirvió esta guía? ☕

Si este contenido te ayudó y querés apoyar a Club Programador para seguir publicando ejercicios, proyectos y guías gratuitas, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal

Sesiones y cookies en PHP: login, logout y manejo de usuarios

Después de aprender a trabajar con formularios, PDO y un CRUD completo, el siguiente paso natural en PHP es entender cómo mantener información de un usuario entre distintas páginas.

Para eso PHP ofrece dos mecanismos fundamentales:

  • sesiones, mediante $_SESSION;
  • cookies, almacenadas en el navegador del usuario.

En esta guía vamos a utilizarlas para construir la base de un sistema de login y logout con PHP y MySQL.

¿Qué problema resuelven las sesiones?

HTTP es un protocolo sin estado. Eso significa que, por defecto, cada petición que hacemos a una página web es independiente de la anterior.

Si un usuario inicia sesión en login.php y después entra a panel.php, necesitamos alguna forma de recordar que ese usuario ya fue autenticado.

Las sesiones permiten guardar información del usuario en el servidor y asociarla a sus siguientes peticiones.

Iniciar una sesión con session_start()

Antes de utilizar $_SESSION debemos iniciar la sesión:

<?php

session_start();

?>

En general, session_start() debe ejecutarse antes de enviar contenido HTML al navegador.

Guardar datos en $_SESSION

Podemos guardar valores como si se tratara de un array asociativo:

<?php

session_start();

$_SESSION['usuario_id'] = 15;
$_SESSION['usuario_nombre'] = 'Ana';

?>

Estos valores estarán disponibles en otras páginas mientras la sesión continúe activa.

Leer datos de sesión

<?php

session_start();

if (isset($_SESSION['usuario_id'])) {
    echo 'Usuario autenticado: ' . $_SESSION['usuario_nombre'];
}

?>

Crear la tabla de usuarios

Para construir un login necesitamos almacenar los usuarios en la base de datos.

CREATE TABLE usuarios (
    id INT AUTO_INCREMENT PRIMARY KEY,
    nombre VARCHAR(100) NOT NULL,
    email VARCHAR(150) NOT NULL UNIQUE,
    password VARCHAR(255) NOT NULL,
    activo BOOLEAN DEFAULT TRUE,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

La columna password tendrá suficiente espacio para almacenar el hash generado por PHP.

Nunca guardar contraseñas en texto plano

Una contraseña no debe almacenarse directamente en la base de datos.

PHP incluye password_hash(), que genera un hash seguro:

<?php

$password = 'MiClaveSegura123';

$hash = password_hash($password, PASSWORD_DEFAULT);

echo $hash;

?>

El resultado será diferente de la contraseña original y es lo que debemos guardar en la base.

Registrar un usuario con PDO

<?php

$nombre = $_POST['nombre'] ?? '';
$email = $_POST['email'] ?? '';
$password = $_POST['password'] ?? '';

$hash = password_hash($password, PASSWORD_DEFAULT);

$sql = 'INSERT INTO usuarios (nombre, email, password)
        VALUES (:nombre, :email, :password)';

$stmt = $pdo->prepare($sql);

$stmt->execute([
    'nombre' => $nombre,
    'email' => $email,
    'password' => $hash
]);

?>

Formulario de login

<form method="POST" action="login.php">

    <label>Email</label>
    <input type="email" name="email" required>

    <label>Contraseña</label>
    <input type="password" name="password" required>

    <button type="submit">Ingresar</button>

</form>

Buscar al usuario por email

Cuando recibimos el formulario, buscamos al usuario mediante una consulta preparada:

<?php

session_start();

$email = $_POST['email'] ?? '';
$password = $_POST['password'] ?? '';

$sql = 'SELECT id, nombre, email, password, activo
        FROM usuarios
        WHERE email = :email
        LIMIT 1';

$stmt = $pdo->prepare($sql);
$stmt->execute([
    'email' => $email
]);

$usuario = $stmt->fetch(PDO::FETCH_ASSOC);

?>

Validar la contraseña con password_verify()

No debemos comparar la contraseña ingresada con el hash utilizando ==.

PHP proporciona password_verify():

if (
    $usuario
    && $usuario['activo']
    && password_verify($password, $usuario['password'])
) {
    // Login correcto
}

Crear la sesión después del login

Si los datos son correctos, guardamos solo la información necesaria en la sesión:

if (
    $usuario
    && $usuario['activo']
    && password_verify($password, $usuario['password'])
) {
    session_regenerate_id(true);

    $_SESSION['usuario_id'] = $usuario['id'];
    $_SESSION['usuario_nombre'] = $usuario['nombre'];

    header('Location: panel.php');
    exit;
}

session_regenerate_id(true) ayuda a reducir el riesgo de ataques de fijación de sesión después de autenticar al usuario.

Ejemplo completo de login.php

<?php

session_start();

require 'conexion.php';

$error = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {

    $email = trim($_POST['email'] ?? '');
    $password = $_POST['password'] ?? '';

    $sql = 'SELECT id, nombre, password, activo
            FROM usuarios
            WHERE email = :email
            LIMIT 1';

    $stmt = $pdo->prepare($sql);

    $stmt->execute([
        'email' => $email
    ]);

    $usuario = $stmt->fetch(PDO::FETCH_ASSOC);

    if (
        $usuario
        && $usuario['activo']
        && password_verify($password, $usuario['password'])
    ) {
        session_regenerate_id(true);

        $_SESSION['usuario_id'] = $usuario['id'];
        $_SESSION['usuario_nombre'] = $usuario['nombre'];

        header('Location: panel.php');
        exit;
    }

    $error = 'Email o contraseña incorrectos';
}

?>

Es mejor mostrar un mensaje genérico como “Email o contraseña incorrectos” en lugar de revelar si el email existe o no.

Proteger una página privada

En panel.php podemos verificar si existe la sesión:

<?php

session_start();

if (!isset($_SESSION['usuario_id'])) {
    header('Location: login.php');
    exit;
}

?>

Después podemos mostrar información del usuario:

<h1>
    Bienvenido,
    <?= htmlspecialchars($_SESSION['usuario_nombre']) ?>
</h1>

Crear logout.php

Para cerrar sesión:

<?php

session_start();

$_SESSION = [];

session_destroy();

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

?>

¿Qué es una cookie?

Una cookie es un pequeño valor almacenado por el navegador y enviado posteriormente al servidor.

Podemos crear una cookie con setcookie():

<?php

setcookie(
    'tema',
    'oscuro',
    [
        'expires' => time() + 3600 * 24 * 30,
        'path' => '/',
        'httponly' => true,
        'samesite' => 'Lax'
    ]
);

?>

Leer una cookie

<?php

$tema = $_COOKIE['tema'] ?? 'claro';

echo $tema;

?>

Sesiones vs cookies

Aunque están relacionadas, no cumplen exactamente la misma función.

Sesiones

  • los datos principales se mantienen en el servidor;
  • son apropiadas para identificar a un usuario autenticado;
  • normalmente el navegador conserva solo un identificador de sesión.

Cookies

  • se almacenan en el navegador;
  • pueden sobrevivir al cierre del navegador según su fecha de expiración;
  • son útiles para preferencias y otras necesidades persistentes;
  • el usuario puede modificarlas, por lo que nunca debemos confiar ciegamente en su contenido.

No guardar contraseñas en cookies

Nunca debemos hacer algo como:

setcookie('password', $password);

Las contraseñas no deben almacenarse en cookies, sesiones, archivos de texto ni registros de log en texto plano.

¿Y la opción “Recordarme”?

Una función de “Recordarme” no debería guardar directamente el identificador del usuario ni su contraseña como mecanismo de autenticación.

Una implementación seria utiliza un token aleatorio almacenado de forma segura en la base de datos y una cookie asociada a ese token.

Ese tema merece una implementación específica y es mejor abordarlo después de comprender bien sesiones, seguridad y autenticación.

Configurar cookies de sesión de forma más segura

En una aplicación servida mediante HTTPS podemos mejorar la configuración de la cookie de sesión:

<?php

session_set_cookie_params([
    'httponly' => true,
    'secure' => true,
    'samesite' => 'Lax'
]);

session_start();

?>

secure debe utilizarse cuando el sitio trabaja mediante HTTPS.

Evitar guardar información innecesaria en la sesión

No necesitamos guardar el registro completo del usuario.

Normalmente alcanza con almacenar identificadores y algunos datos mínimos:

$_SESSION['usuario_id'] = $usuario['id'];
$_SESSION['usuario_nombre'] = $usuario['nombre'];

Si necesitamos información actualizada, podemos consultar nuevamente la base utilizando el identificador.

Errores comunes

  • olvidar ejecutar session_start();
  • enviar HTML antes de utilizar header() o setcookie();
  • guardar contraseñas en texto plano;
  • comparar contraseñas sin password_verify();
  • guardar datos sensibles dentro de cookies;
  • no regenerar el identificador de sesión después del login;
  • no verificar la sesión en cada página privada;
  • mostrar mensajes que permiten descubrir si una cuenta existe;
  • confiar en valores provenientes de cookies como si fueran datos seguros.

Ejercicio práctico

Creá un pequeño sistema con estas páginas:

  1. registro.php: crear un usuario utilizando password_hash();
  2. login.php: validar email y contraseña;
  3. panel.php: permitir el acceso solo a usuarios autenticados;
  4. logout.php: finalizar la sesión;
  5. una cookie para recordar una preferencia como el tema claro u oscuro.

¿Qué sigue?

En la próxima guía podemos avanzar con autenticación y autorización en PHP: roles, permisos, protección de rutas y control de acceso para distintos tipos de usuarios.

¿Te sirvió esta guía? ☕

Si este contenido te ayudó y querés apoyar a Club Programador para seguir publicando ejercicios, proyectos y guías gratuitas, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal