Tabla de contenido


Introducción: Por qué PHP y MySQL dominan el Backend

Si HTML y CSS son la cara de la web, y JavaScript es el cerebro interactivo, entonces PHP y MySQL son el sistema digestivo — procesan, almacenan y entregan datos.

PHP es el lenguaje de backend más usado en la historia de la web. Fue creado en 1994 por Rasmus Lerdorf y desde entonces ha evolucionado enormemente. PHP 8.3+ incluye:

  • Tipado estricto opcional
  • JIT (Just-In-Time) compiler para rendimiento
  • Constructor property promotion
  • Match expressions (mejor que switch)
  • Named arguments
  • Nullsafe operator

MySQL es el sistema de gestión de bases de datos relacional más popular del mundo. Almacena datos de forma estructurada en tablas que se relacionan entre sí.

Juntos, PHP + MySQL forman la base de:

  • WordPress (43% de todos los sitios web)
  • Laravel (el framework PHP más popular)
  • WooCommerce, Magento, Drupal
  • Miles de aplicaciones empresariales

Aprender PHP y MySQL te abre las puertas al desarrollo backend real: crear APIs, manejar usuarios, procesar pagos, gestionar inventarios y construir aplicaciones completas.

Si quieres empezar con un tutorial práctico, revisa nuestro CRUD completo con PHP y MySQL.


Parte 1: PHP Moderno

1.1 Tu primer script PHP

PHP se ejecuta del lado del servidor. A diferencia de JavaScript que corre en el navegador, PHP procesa la petición en el servidor y devuelve HTML al navegador.

Para ejecutar PHP necesitas un servidor local. La forma más fácil:

  1. Instala XAMPP desde apachefriends.org
  2. Inicia Apache y MySQL desde el panel
  3. Crea un archivo hola.php en la carpeta htdocs
  4. Accede a http://localhost/hola.php
<?php
// Tu primer script PHP
echo "¡Hola desde 8devmx!";
?>

El <?php abre el bloque de código PHP y ?> lo cierra. En archivos que son puro PHP, puedes omitir el cierre ?> (es buena práctica).

1.2 Variables y tipos de datos

En PHP, las variables siempre empiezan con $:

<?php
// Strings
$nombre = "8devmx";
$sitio = 'Desarrollo web en español';

// Números
$edad = 5;
$precio = 19.99;

// Boolean
$esGratis = true;
$tieneCurso = false;

// Array (lista)
$tecnologias = ["PHP", "MySQL", "JavaScript", "Laravel"];

// Array asociativo (como objetos en JS)
$curso = [
    "nombre" => "PHP y MySQL",
    "nivel" => "Principiante",
    "duracion" => 40,
];

// Null
$resultado = null;

// Interpolar strings (con comillas dobles)
$saludo = "Bienvenido a $nombre";  // "Bienvenido a 8devmx"
$detalle = "El curso de {$curso['nombre']} dura {$curso['duracion']} horas";

// Verificar tipos
gettype($nombre);    // "string"
gettype($precio);    // "double"
gettype($tecnologias); // "array"

// PHP 8+: Match expression (mejor que switch)
$mensaje = match ($curso['nivel']) {
    'Principiante' => 'Empieza desde cero',
    'Intermedio' => 'Ya tienes bases',
    'Avanzado' => 'Eres un experto',
    default => 'Nivel desconocido',
};
?>

Para más tipos de datos, revisa Tipos de datos en PHP.

1.3 Condicionales y operadores

<?php
$nota = 85;

// if / elseif / else
if ($nota >= 90) {
    echo "Excelente";
} elseif ($nota >= 70) {
    echo "Aprobado";
} else {
    echo "Reprobado";
}

// Operador ternario
$estado = $nota >= 70 ? "Aprobado" : "Reprobado";

// Null coalescing (PHP 7+)
$nombre = $usuario ?? "Anónimo";

// Nullsafe operator (PHP 8+)
$ciudad = $usuario?->direccion?->ciudad;

// Comparación
5 === 5;   // true (igualdad estricta)
5 == "5";  // true (igualdad débil — evitar)
5 !== 3;   // true (desigualdad estricta)

// Lógicos
$esActivo && $tienePermiso;  // AND
$esAdmin || $esModerador;    // OR
!$estaBaneado;               // NOT
?>

1.4 Arrays en PHP

<?php
// Array indexado
$frutas = ["Manzana", "Banana", "Naranja"];
echo $frutas[0]; // "Manzana"

// Array asociativo
$usuario = [
    "nombre" => "Abraham",
    "email" => "abraham@8devmx.com",
    "rol" => "admin",
];
echo $usuario["nombre"]; // "Abraham"

// Agregar elementos
$frutas[] = "Uva"; // Agrega al final

// Recorrer arrays
foreach ($frutas as $fruta) {
    echo $fruta . "\n";
}

// Con clave y valor
foreach ($usuario as $clave => $valor) {
    echo "$clave: $valor\n";
}

// Funciones útiles para arrays
count($frutas);              // 4 (cantidad de elementos)
in_array("Banana", $frutas); // true
array_keys($usuario);        // ["nombre", "email", "rol"]
array_values($usuario);      // ["Abraham", "abraham@...", "admin"]

// Filtrar
$admin = array_filter($usuarios, fn($u) => $u["rol"] === "admin");

// Transformar
$nombres = array_map(fn($u) => $u["nombre"], $usuarios);

// PHP 8+: Named arguments
$frutasOrdenadas = array_slice(
    array: $frutas,
    offset: 0,
    length: 2,
);
?>

Para más sobre arrays en PHP, visita Arrays en PHP.

1.5 Funciones

<?php
// Función básica
function saludar(string $nombre): string {
    return "Hola, $nombre!";
}

echo saludar("8devmx"); // "Hola, 8devmx!"

// Con valor por defecto
function crearTitulo(string $texto, string $separador = " | "): string {
    return $texto . $separador . "8devmx";
}

// Arrow function (PHP 7.4+)
$doble = fn($n) => $n * 2;

// PHP 8+: Constructor property promotion
class Usuario {
    public function __construct(
        public string $nombre,
        public string $email,
        public bool $activo = true,
    ) {}
}

$user = new Usuario("Abraham", "hola@8devmx.com");
echo $user->nombre; // "Abraham"
?>

1.6 Loops

<?php
// for
for ($i = 0; $i < 5; $i++) {
    echo "Número: $i\n";
}

// foreach (el más usado en PHP)
$colores = ["rojo", "verde", "azul"];
foreach ($colores as $color) {
    echo $color . "\n";
}

// while
$intentos = 0;
while ($intentos < 3) {
    echo "Intento " . ($intentos + 1) . "\n";
    $intentos++;
}

// do...while (se ejecuta al menos una vez)
$num = 10;
do {
    echo "El número es: $num\n";
    $num--;
} while ($num > 5);
?>

Para ver los ciclos en acción, revisa 3 Ciclos en PHP.

1.7 Programación orientada a objetos

PHP soporta OOP completa con clases, herencia, interfaces y traits:

<?php
// Clase básica
class Curso {
    // Propiedades
    public string $nombre;
    public int $duracion;
    protected array $temas = [];

    // Constructor (PHP 8+)
    public function __construct(string $nombre, int $duracion) {
        $this->nombre = $nombre;
        $this->duracion = $duracion;
    }

    // Método
    public function agregarTema(string $tema): void {
        $this->temas[] = $tema;
    }

    // Método con retorno
    public function getInfo(): string {
        return "{$this->nombre} - {$this->duracion} horas - " . count($this->temas) . " temas";
    }
}

// Herencia
class CursoPremium extends Curso {
    public float $precio;

    public function __construct(string $nombre, int $duracion, float $precio) {
        parent::__construct($nombre, $duracion);
        $this->precio = $precio;
    }
}

// Uso
$php = new Curso("PHP y MySQL", 40);
$php->agregarTema("Variables y tipos");
$php->agregarTema("PDO y bases de datos");
echo $php->getInfo();
?>

Para profundizar en POO en PHP, revisa Encapsulamiento y Herencia en PHP.


Parte 2: MySQL y Bases de Datos

2.1 ¿Qué es una base de datos relacional?

Una base de datos relacional organiza la información en tablas que pueden relacionarse entre sí. Piensa en una hoja de cálculo, pero más poderosa.

Conceptos clave:

ConceptoDescripciónEjemplo
TablaColección de datos del mismo tipousuarios, productos
Columna (campo)Un tipo específico de datonombre, email, edad
Fila (registro)Una entrada individualUn usuario específico
Primary Key (PK)Identificador únicoid autoincremental
Foreign Key (FK)Enlace a otra tablacategoria_idcategorias.id
ÍndiceAcelera búsquedasÍndice en email

2.2 Crear bases de datos y tablas

-- Crear base de datos
CREATE DATABASE mi_aplicacion CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- Usar la base de datos
USE mi_aplicacion;

-- Crear tabla de usuarios
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,
    rol ENUM('admin', 'usuario', 'moderador') DEFAULT 'usuario',
    activo BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_email (email),
    INDEX idx_rol (rol)
) ENGINE=InnoDB;

-- Crear tabla de posts
CREATE TABLE posts (
    id INT AUTO_INCREMENT PRIMARY KEY,
    usuario_id INT NOT NULL,
    titulo VARCHAR(200) NOT NULL,
    slug VARCHAR(200) NOT NULL UNIQUE,
    contenido TEXT NOT NULL,
    publicado BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (usuario_id) REFERENCES usuarios(id) ON DELETE CASCADE,
    INDEX idx_slug (slug),
    INDEX idx_publicado (publicado)
) ENGINE=InnoDB;

Tipos de datos más usados:

TipoUsoEjemplo
INTNúmeros enterosIDs, cantidades
VARCHAR(n)Texto variable hasta n caracteresNombres, emails
TEXTTexto largoContenido de posts
BOOLEANVerdadero/falsopublicado, activo
TIMESTAMPFecha y horacreated_at, updated_at
DECIMAL(10,2)Números con decimalesPrecios
ENUM(...)Valor de una lista limitadarol, estado

2.3 Consultas SQL fundamentales

-- INSERT: Crear un registro
INSERT INTO usuarios (nombre, email, password, rol)
VALUES ('Abraham', 'abraham@8devmx.com', '$2y$10$...', 'admin');

-- SELECT: Leer registros
SELECT * FROM usuarios;
SELECT nombre, email FROM usuarios WHERE activo = TRUE;
SELECT nombre FROM usuarios WHERE rol = 'admin' ORDER BY nombre ASC;

-- SELECT con límites
SELECT * FROM posts ORDER BY created_at DESC LIMIT 10;
SELECT * FROM posts ORDER BY created_at DESC LIMIT 10 OFFSET 20; -- Página 3

-- UPDATE: Actualizar registros
UPDATE usuarios
SET rol = 'moderador', updated_at = NOW()
WHERE id = 5;

-- DELETE: Eliminar registros
DELETE FROM posts WHERE id = 15;
DELETE FROM usuarios WHERE activo = FALSE AND created_at < '2025-01-01';

Para entender más sobre queries, revisa Tipos de comandos SQL.

2.4 JOINs: Unir tablas

Los JOINs permiten combinar datos de múltiples tablas en una sola consulta:

-- INNER JOIN: Solo registros que tienen coincidencia en ambas tablas
SELECT
    u.nombre,
    u.email,
    p.titulo,
    p.created_at
FROM usuarios u
INNER JOIN posts p ON u.id = p.usuario_id
WHERE p.publicado = TRUE;

-- LEFT JOIN: Todos los usuarios, incluso sin posts
SELECT
    u.nombre,
    COUNT(p.id) as total_posts
FROM usuarios u
LEFT JOIN posts p ON u.id = p.usuario_id
GROUP BY u.id
ORDER BY total_posts DESC;

-- RIGHT JOIN: Todos los posts, incluso sin usuario
SELECT
    p.titulo,
    u.nombre as autor
FROM posts p
RIGHT JOIN usuarios u ON p.usuario_id = u.id;

-- Múltiples JOINs
SELECT
    p.titulo,
    u.nombre as autor,
    c.nombre as categoria
FROM posts p
INNER JOIN usuarios u ON p.usuario_id = u.id
LEFT JOIN categorias c ON p.categoria_id = c.id
WHERE p.publicado = TRUE
ORDER BY p.created_at DESC;

Para dominar JOINs completamente, revisa JOINs en MySQL.

2.5 Índices y rendimiento

Los índices aceleran las búsquedas pero ralentizan las inserciones. Úsalos sabiamente:

-- Crear índice en columna que se busca frecuentemente
CREATE INDEX idx_email ON usuarios(email);

-- Índice compuesto (para búsquedas combinadas)
CREATE INDEX idx_rol_activo ON usuarios(rol, activo);

-- Ver índices existentes
SHOW INDEX FROM usuarios;

-- EXPLAIN: Analizar cómo MySQL ejecuta una consulta
EXPLAIN SELECT * FROM usuarios WHERE email = 'test@test.com';

Reglas de oro para índices:

  • Indexa columnas que usas en WHERE, JOIN, ORDER BY
  • No indexes columnas con pocos valores únicos (como BOOLEAN)
  • Usa EXPLAIN para verificar si tus queries usan los índices
  • Demasiados índices ralentizan las inserciones

Para más sobre rendimiento, revisa Índices en MySQL y rendimiento.


Parte 3: PHP + MySQL Juntos

3.1 Conexión con PDO

PDO (PHP Data Objects) es la forma moderna y segura de conectar PHP con MySQL:

<?php
// db.php — Conexión centralizada
$host = 'localhost';
$db   = 'mi_aplicacion';
$user = 'root';
$pass = '';

try {
    $pdo = new PDO(
        "mysql:host=$host;dbname=$db;charset=utf8mb4",
        $user,
        $pass,
        [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES => false,
        ]
    );
} catch (PDOException $e) {
    // En producción: loggear el error, no mostrarlo al usuario
    error_log($e->getMessage());
    die("Error de conexión. Intenta más tarde.");
}
?>

¿Por qué PDO y no mysql_connect?

  • Usa sentencias preparadas (previene SQL injection)
  • Compatible con múltiples bases de datos (MySQL, PostgreSQL, SQLite)
  • API orientada a objetos
  • Manejo de errores con excepciones

Para la conexión detallada, revisa Conexión a base de datos con PDO.

3.2 Insertar datos (Create)

<?php
require 'db.php';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $nombre = trim($_POST['nombre']);
    $email = trim($_POST['email']);
    $password = password_hash($_POST['password'], PASSWORD_DEFAULT);

    // Sentencia preparada — PREVIENE SQL INJECTION
    $stmt = $pdo->prepare(
        "INSERT INTO usuarios (nombre, email, password) VALUES (:nombre, :email, :password)"
    );
    $stmt->execute([
        ':nombre' => $nombre,
        ':email' => $email,
        ':password' => $password,
    ]);

    $nuevoId = $pdo->lastInsertId();
    echo "Usuario creado con ID: $nuevoId";
}
?>

3.3 Leer datos (Read)

<?php
require 'db.php';

// Obtener todos los usuarios
$stmt = $pdo->query("SELECT id, nombre, email, rol, created_at FROM usuarios ORDER BY created_at DESC");
$usuarios = $stmt->fetchAll();

// Obtener un solo usuario por ID
$stmt = $pdo->prepare("SELECT * FROM usuarios WHERE id = :id");
$stmt->execute([':id' => 5]);
$usuario = $stmt->fetch();

// Contar registros
$stmt = $pdo->query("SELECT COUNT(*) as total FROM usuarios WHERE activo = TRUE");
$total = $stmt->fetch()['total'];

// Paginación
$porPagina = 10;
$pagina = isset($_GET['pagina']) ? (int)$_GET['pagina'] : 1;
$offset = ($pagina - 1) * $porPagina;

$stmt = $pdo->prepare(
    "SELECT id, nombre, email FROM usuarios ORDER BY created_at DESC LIMIT :limit OFFSET :offset"
);
$stmt->bindValue(':limit', $porPagina, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$usuarios = $stmt->fetchAll();
?>

3.4 Actualizar datos (Update)

<?php
require 'db.php';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $id = (int) $_POST['id'];
    $nombre = trim($_POST['nombre']);
    $email = trim($_POST['email']);

    // Verificar que el email no exista en otro usuario
    $stmt = $pdo->prepare("SELECT id FROM usuarios WHERE email = :email AND id != :id");
    $stmt->execute([':email' => $email, ':id' => $id]);

    if ($stmt->fetch()) {
        $error = "Ese email ya está registrado";
    } else {
        $stmt = $pdo->prepare(
            "UPDATE usuarios SET nombre = :nombre, email = :email WHERE id = :id"
        );
        $stmt->execute([
            ':nombre' => $nombre,
            ':email' => $email,
            ':id' => $id,
        ]);

        $mensaje = "Usuario actualizado correctamente";
    }
}
?>

3.5 Eliminar datos (Delete)

<?php
require 'db.php';

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['id'])) {
    $id = (int) $_POST['id'];

    // SOLO eliminar por ID — nunca sin verificación
    $stmt = $pdo->prepare("DELETE FROM usuarios WHERE id = :id");
    $stmt->execute([':id' => $id]);

    // ON DELETE CASCADE elimina automáticamente los posts del usuario
    header("Location: usuarios.php?msg=eliminado");
    exit;
}
?>

3.6 Seguridad: Prevenir SQL Injection y XSS

<?php
// === PREVENIR SQL INJECTION ===
// ✅ CORRECTO: Sentencias preparadas
$stmt = $pdo->prepare("SELECT * FROM usuarios WHERE email = :email");
$stmt->execute([':email' => $email]);

// ❌ INCORRECTO: Concatenar variables en la consulta
// $resultado = $pdo->query("SELECT * FROM usuarios WHERE email = '$email'");

// === PREVENIR XSS ===
// ✅ CORRECTO: Escapar al mostrar
echo htmlspecialchars($usuario['nombre'], ENT_QUOTES, 'UTF-8');

// ❌ INCORRECTO: Mostrar sin escapar
// echo $usuario['nombre']; // Vulnerable a <script>alert('XSS')</script>

// === PREVENIR CSRF ===
// Generar token en el formulario
session_start();
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));

// Verificar en el procesamiento
if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
    die("Token CSRF inválido");
}

// === PASSWORD HASHING ===
// Al registrar
$hash = password_hash($password, PASSWORD_DEFAULT);

// Al verificar login
if (password_verify($password, $hash)) {
    echo "Login exitoso";
}

// === VALIDACIÓN DE INPUT ===
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
if (!$email) {
    $errores[] = "Email inválido";
}
?>

Parte 4: Proyecto CRUD Completo

Vamos a construir un sistema de gestión de tutoriales completo:

<?php
// index.php — Listar tutoriales
require 'db.php';

$stmt = $pdo->query("
    SELECT t.id, t.titulo, t.descripcion, t.nivel,
           c.nombre as categoria, t.created_at
    FROM tutoriales t
    LEFT JOIN categorias c ON t.categoria_id = c.id
    ORDER BY t.created_at DESC
");
$tutoriales = $stmt->fetchAll();
?>
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Gestión de Tutoriales - 8devmx</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'DM Sans', sans-serif; background: #111213; color: #E8E6E1; padding: 2rem; }
        h1 { font-family: 'Space Grotesk', sans-serif; margin-bottom: 1.5rem; }
        .btn { display: inline-block; padding: 0.5rem 1rem; background: #E53E3E; color: white; text-decoration: none; border-radius: 0.5rem; font-weight: 600; }
        .btn:hover { background: #C53030; }
        table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
        th, td { padding: 0.75rem; text-align: left; border-bottom: 1px solid rgba(255,255,255,0.08); }
        th { background: #1A1B1D; }
        .nivel { padding: 0.25rem 0.5rem; border-radius: 0.25rem; font-size: 0.75rem; }
        .nivel.basico { background: #22c55e20; color: #22c55e; }
        .nivel.intermedio { background: #F59E0B20; color: #F59E0B; }
        .nivel.avanzado { background: #E53E3E20; color: #E53E3E; }
        .acciones { display: flex; gap: 0.5rem; }
        .btn-sm { padding: 0.25rem 0.5rem; font-size: 0.75rem; }
        .btn-edit { background: #3ABFF8; }
        .btn-delete { background: #E53E3E; }
    </style>
</head>
<body>
    <h1>📚 Gestión de Tutoriales</h1>
    <a href="crear.php" class="btn">+ Nuevo Tutorial</a>

    <table>
        <thead>
            <tr>
                <th>Título</th>
                <th>Categoría</th>
                <th>Nivel</th>
                <th>Fecha</th>
                <th>Acciones</th>
            </tr>
        </thead>
        <tbody>
            <?php if (empty($tutoriales)): ?>
                <tr><td colspan="5">No hay tutoriales. ¡Crea el primero!</td></tr>
            <?php else: ?>
                <?php foreach ($tutoriales as $t): ?>
                    <tr>
                        <td><?= htmlspecialchars($t['titulo']) ?></td>
                        <td><?= htmlspecialchars($t['categoria'] ?? 'Sin categoría') ?></td>
                        <td><span class="nivel <?= $t['nivel'] ?>"><?= ucfirst($t['nivel']) ?></span></td>
                        <td><?= date('d/m/Y', strtotime($t['created_at'])) ?></td>
                        <td class="acciones">
                            <a href="editar.php?id=<?= $t['id'] ?>" class="btn btn-sm btn-edit">Editar</a>
                            <form method="POST" action="eliminar.php" style="display:inline" onsubmit="return confirm('¿Eliminar?')">
                                <input type="hidden" name="id" value="<?= $t['id'] ?>">
                                <input type="hidden" name="csrf" value="<?= $_SESSION['csrf_token'] ?>">
                                <button type="submit" class="btn btn-sm btn-delete">Eliminar</button>
                            </form>
                        </td>
                    </tr>
                <?php endforeach; ?>
            <?php endif; ?>
        </tbody>
    </table>
</body>
</html>
<?php
// crear.php — Crear tutorial
require 'db.php';
session_start();
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));

$errores = [];

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf'] ?? '')) {
        die("Token CSRF inválido");
    }

    $titulo = trim($_POST['titulo'] ?? '');
    $descripcion = trim($_POST['descripcion'] ?? '');
    $nivel = $_POST['nivel'] ?? '';
    $categoria_id = (int)($_POST['categoria_id'] ?? 0);

    // Validaciones
    if (empty($titulo)) $errores[] = "El título es obligatorio";
    if (strlen($titulo) > 200) $errores[] = "El título no puede tener más de 200 caracteres";
    if (empty($descripcion)) $errores[] = "La descripción es obligatoria";
    if (!in_array($nivel, ['basico', 'intermedio', 'avanzado'])) $errores[] = "Nivel inválido";

    if (empty($errores)) {
        $stmt = $pdo->prepare(
            "INSERT INTO tutoriales (titulo, slug, descripcion, nivel, categoria_id)
             VALUES (:titulo, :slug, :descripcion, :nivel, :categoria_id)"
        );
        $stmt->execute([
            ':titulo' => $titulo,
            ':slug' => strtolower(str_replace(' ', '-', $titulo)),
            ':descripcion' => $descripcion,
            ':nivel' => $nivel,
            ':categoria_id' => $categoria_id ?: null,
        ]);

        header("Location: index.php?msg=creado");
        exit;
    }
}

// Obtener categorías para el select
$categorias = $pdo->query("SELECT id, nombre FROM categorias ORDER BY nombre")->fetchAll();
?>
<!-- HTML del formulario aquí (similar al index pero con form) -->

Este proyecto demuestra:

  • PDO con sentencias preparadas
  • CRUD completo (Create, Read, Update, Delete)
  • Prevención de SQL injection y XSS
  • Tokens CSRF
  • Validación de formularios
  • JOINs para obtener datos relacionados
  • HTML semántico con tablas

Parte 5: Siguientes Pasos

1. Aprende Laravel

Una vez que domines PHP y MySQL puro, Laravel es el siguiente paso natural. Es el framework PHP más popular y te permite construir aplicaciones empresariales rápidamente:

// Así de simple es crear un endpoint en Laravel
Route::get('/api/tutoriales', [TutorialController::class, 'index']);
Route::post('/api/tutoriales', [TutorialController::class, 'store']);

Te recomendamos:

2. Construye APIs REST

Aprende a crear APIs que el frontend consume con JavaScript o frameworks como React:

// API REST simple en PHP puro
header('Content-Type: application/json');

$method = $_SERVER['REQUEST_METHOD'];

switch ($method) {
    case 'GET':
        // Listar recursos
        break;
    case 'POST':
        // Crear recurso
        break;
    case 'PUT':
        // Actualizar recurso
        break;
    case 'DELETE':
        // Eliminar recurso
        break;
}

Para entender las APIs a fondo, revisa ¿Qué es una API REST? y Documentar API con Swagger.

3. Aprende sobre sesiones y autenticación

<?php
session_start();

// Login
if (password_verify($password, $hash)) {
    $_SESSION['usuario_id'] = $user['id'];
    $_SESSION['usuario_nombre'] = $user['nombre'];
    session_regenerate_id(true); // Prevenir session fixation
}

// Verificar autenticación
if (!isset($_SESSION['usuario_id'])) {
    header("Location: login.php");
    exit;
}

// Logout
session_destroy();
?>

4. Sigue la Ruta del Ninja

En 8devmx tenemos una ruta completa para backend:


Resumen de esta guía:

ConceptoImportanciaTiempo estimado
Variables y tiposEsencial2-3 días
ArraysEsencial3-5 días
FuncionesEsencial3-5 días
POO en PHPMuy importante1 semana
MySQL básicoEsencial1 semana
Consultas SQLEsencial1 semana
JOINsMuy importante3-5 días
ÍndicesImportante2-3 días
PDOEsencial1 semana
CRUD completoEsencial1-2 semanas
Seguridad (XSS, SQLi)Esencial3-5 días
SesionesImportante2-3 días

Total estimado: 2-3 meses con práctica diaria de 1-2 horas.

El desarrollo backend es donde tu aplicación cobra vida real. Con PHP y MySQL puedes construir desde un simple blog hasta una plataforma de e-commerce completa. Empieza con los fundamentos, construye proyectos, y no tengas miedo de romper cosas — así es como se aprende.