Programación · n.º 13 entre los lenguajes más usados

Aprende PHP desde cero

El lenguaje de servidor detrás de WordPress y de buena parte de la web.

  • 15 lecciones
  • 19 ejercicios interactivos
  • 28 conceptos clave
  • 17,7 % lo usa*

Qué es PHP y para qué sirve

PHP se ejecuta en el servidor: genera la página HTML antes de enviarla al navegador. Con él están hechos WordPress, Wikipedia (MediaWiki) y frameworks como Laravel y Symfony. Las versiones modernas (PHP 8) tienen tipos, clases y un rendimiento muy distinto al del PHP de hace años.

  • Webs dinámicas y tiendas online
  • WordPress y otros gestores de contenidos
  • APIs y paneles de administración

Para empezar: Instala PHP desde php.net (o con un paquete como XAMPP) y lanza el servidor de pruebas con php -S localhost:8000.

Nivel 1 Básico

Lección 1 · Básico

Tu primer script

El código PHP va entre <?php y ?>, y puede mezclarse con HTML: el servidor ejecuta el PHP y envía al navegador solo el resultado. echo escribe texto en la página.

PHP trae un servidor web de pruebas: en la carpeta del proyecto, php -S localhost:8000 y abre esa dirección en el navegador.

Ejemplo práctico
<!-- index.php -->
<!DOCTYPE html>
<html lang="es">
<body>
  <h1><?php echo "¡Hola, mundo!"; ?></h1>
  <p>Hoy es <?= date("d/m/Y") ?></p>
</body>
</html>

Ejercicio

¿Dónde se ejecuta el código PHP?

Ver la solución

Respuesta: En el servidor, antes de enviar la página. El servidor ejecuta el PHP y el navegador solo recibe el HTML resultante.

Lección 2 · Básico

Variables y texto

Las variables empiezan por $ y no se declaran con tipo. Los textos se unen con el punto ..

Entre comillas dobles, las variables se sustituyen por su valor; entre comillas simples, no.

Ejemplo práctico
<?php
$nombre = "Ana";
$edad = 30;
$edad++;

echo "Hola, $nombre. Tienes $edad años.\n";
echo 'Hola, $nombre' . "\n";        // Hola, $nombre
echo "Total: " . (3 * 19.99) . " €\n";

Ejercicio

Completa el operador que une dos textos.

echo "Hola, "  $nombre;
Ver la solución

Respuesta: .. En PHP el punto . concatena cadenas; el + es solo para sumar números.

Lección 3 · Básico

Condiciones y bucles

if, elseif y else deciden qué se ejecuta. Compara con ===, que también comprueba el tipo, para evitar sorpresas con las conversiones automáticas.

foreach recorre un array, for usa un contador y while repite mientras se cumpla una condición.

Ejemplo práctico
<?php
$nota = 7;

if ($nota >= 9) {
    echo "Sobresaliente\n";
} elseif ($nota >= 5) {
    echo "Aprobado\n";
} else {
    echo "Suspenso\n";
}

for ($i = 1; $i <= 3; $i++) {
    echo "Vuelta $i\n";
}

Ejercicio

¿Qué comparación comprueba valor y tipo a la vez?

Ver la solución

Respuesta: ===. === exige el mismo valor y el mismo tipo; == convierte los tipos antes de comparar.

Encuentra los errores

Con la edad 17 este script muestra «Casi: te falta un año» y parece que funciona, pero tiene 2 errores que aparecerían con otras edades.

Ver la solución
  • Línea 6: En PHP los textos se unen con el punto .; + intenta sumar números y con un texto como «Hola, » da un TypeError. Corrección: echo "Hola, " . $nombre;
  • Línea 7: = asigna 17 a $edad y la condición siempre se cumple, tenga la edad que tenga. Para comparar se usa ===. Corrección: } elseif ($edad === 17) {

Lección 4 · Básico

Arrays

Un array puede ser una lista (["pan", "leche"]) o asociativo, con claves de texto (["teclado" => 10]). count() dice cuántos elementos tiene.

foreach recorre los valores, o las claves y los valores a la vez con $clave => $valor.

Ejemplo práctico
<?php
$compra = ["pan", "leche"];
$compra[] = "huevos";              // añade al final
echo count($compra) . "\n";        // 3

$stock = ["teclado" => 10, "raton" => 0];
foreach ($stock as $producto => $unidades) {
    echo "$producto: $unidades\n";
}

Ejercicio

¿Qué hace $compra[] = "huevos";?

Ver la solución

Respuesta: Añade «huevos» al final. Con corchetes vacíos, PHP añade el valor en la siguiente posición libre.

Escribe el código

Escribe function totalCarrito(array $carrito): float: cada elemento del carrito es un array con las claves 'precio' y 'cantidad'. Devuelve la suma de precio × cantidad de todos los productos, recorriéndolo con foreach.

Tab escribe una sangría; para salir del editor con el teclado, pulsa Esc y luego Tab. Ctrl + Intro corrige.

Ver la solución
<?php
function totalCarrito(array $carrito): float {
    $total = 0.0;
    foreach ($carrito as $producto) {
        $total += $producto['precio'] * $producto['cantidad'];
    }
    return $total;
}

Cada vuelta del foreach da un producto (un array asociativo), y se suma su precio por su cantidad. Con un carrito vacío devuelve 0.

Nivel 2 Intermedio

Lección 5 · Intermedio

Funciones con tipos

Las funciones se declaran con function. Desde PHP 7 puedes indicar el tipo de los parámetros y del valor devuelto, y con declare(strict_types=1); PHP los respeta sin convertir.

Los parámetros pueden tener valor por defecto y se pueden pasar por nombre (PHP 8).

Ejemplo práctico
<?php
declare(strict_types=1);

function precioFinal(float $precio, float $iva = 21): float
{
    return round($precio * (1 + $iva / 100), 2);
}

echo precioFinal(100) . "\n";            // 121
echo precioFinal(precio: 100, iva: 10);  // 110

Ejercicio

¿Qué hace declare(strict_types=1);?

Ver la solución

Respuesta: Hace que PHP no convierta los tipos de los argumentos. Con tipos estrictos, pasar un texto donde se espera float da un error en lugar de convertirlo.

Escribe el código

Escribe function precioConDescuento(float $precio, int $porcentaje = 10): float: devuelve el precio con ese porcentaje de descuento, redondeado a 2 decimales con round.

Tab escribe una sangría; para salir del editor con el teclado, pulsa Esc y luego Tab. Ctrl + Intro corrige.

Ver la solución
<?php
function precioConDescuento(float $precio, int $porcentaje = 10): float {
    return round($precio * (100 - $porcentaje) / 100, 2);
}

Con un 10 % de descuento queda el 90 % del precio: $precio * (100 - $porcentaje) / 100. Los tipos hacen que PHP rechace, por ejemplo, un texto donde espera un número.

Lección 6 · Intermedio

Formularios y seguridad

Los datos de un formulario enviado con method="post" llegan en el array $_POST. Nunca confíes en ellos: valídalos antes de usarlos.

Para mostrar en la página algo que escribió el usuario, pásalo por htmlspecialchars(). Así un <script> se muestra como texto y no se ejecuta (se evita el ataque XSS).

Ejemplo práctico
<?php
$nombre = trim($_POST["nombre"] ?? "");

if ($nombre === "") {
    echo "Escribe tu nombre";
} else {
    echo "Hola, " . htmlspecialchars($nombre, ENT_QUOTES, "UTF-8");
}
?>
<form method="post">
  <label for="nombre">Nombre</label>
  <input id="nombre" name="nombre">
  <button>Enviar</button>
</form>

Ejercicio

Completa la función que evita que el texto del usuario se interprete como HTML.

echo ($comentario);
Ver la solución

Respuesta: htmlspecialchars. htmlspecialchars convierte <, >, & y las comillas en entidades HTML inofensivas.

Conceptos clave

Lección 7 · Intermedio

Clases y objetos

Una clase agrupa datos (propiedades) y funciones (métodos). Con la promoción de propiedades del constructor (PHP 8) declaras y asignas las propiedades en una sola línea.

$this es el propio objeto, y la flecha -> accede a sus propiedades y métodos.

Ejemplo práctico
<?php
class Cuenta
{
    public function __construct(
        public readonly string $titular,
        private float $saldo = 0,
    ) {}

    public function ingresar(float $cantidad): void
    {
        $this->saldo += $cantidad;
    }

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

$c = new Cuenta("Ana");
$c->ingresar(50);
echo $c->titular . ": " . $c->saldo();   // Ana: 50

Ejercicio

¿Con qué operador llamas a un método de un objeto en PHP?

Ver la solución

Respuesta: ->. $objeto->metodo() llama a un método de ese objeto; :: es para lo estático.

Encuentra los errores

Este script debería mostrar 1, pero falla. Tiene 2 errores al usar las propiedades del objeto.

Ver la solución
  • Línea 6: $valor es una variable local nueva (vacía), no la propiedad del objeto. Dentro de la clase, las propiedades se usan con $this->. Corrección: $this->valor++;
  • Línea 16: valor es privada: desde fuera de la clase no se puede leer directamente. Para eso está el método público valor(). Corrección: echo $c->valor();

Nivel 3 Avanzado

Lección 8 · Avanzado

Bases de datos con PDO

PDO es la forma estándar de conectar PHP con MySQL, PostgreSQL o SQLite. Usa siempre consultas preparadas: escribes huecos y pasas los valores aparte.

Así evitas la inyección SQL, porque los datos del usuario nunca se mezclan con el código de la consulta.

Ejemplo práctico
<?php
$pdo = new PDO("sqlite:tienda.db", options: [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);

$consulta = $pdo->prepare("SELECT nombre, precio FROM productos WHERE precio < :max");
$consulta->execute(["max" => 20]);

foreach ($consulta as $fila) {
    echo $fila["nombre"] . ": " . $fila["precio"] . "\n";
}

Ejercicio

¿Por qué usar prepare() y execute() en lugar de pegar el valor en la consulta?

Ver la solución

Respuesta: Para evitar la inyección SQL. Los valores van separados de la consulta, así que no pueden cambiar lo que hace.

Lección 9 · Avanzado

Composer y la carga automática

Composer es el gestor de paquetes de PHP: composer require paquete descarga una biblioteca en la carpeta vendor y la apunta en composer.json.

Incluyendo vendor/autoload.php, las clases se cargan solas cuando las usas, sin escribir un require por archivo.

Ejemplo práctico
# En la terminal
composer init
composer require monolog/monolog

<?php
require __DIR__ . "/vendor/autoload.php";

use Monolog\Logger;
use Monolog\Handler\StreamHandler;

$log = new Logger("tienda");
$log->pushHandler(new StreamHandler(__DIR__ . "/app.log"));
$log->info("Pedido recibido", ["id" => 42]);

Ejercicio

Completa el archivo que activa la carga automática de las bibliotecas.

require __DIR__ . "/vendor/.php";
Ver la solución

Respuesta: autoload. vendor/autoload.php, generado por Composer, carga cada clase la primera vez que se usa.

Lección 10 · Avanzado

Errores y excepciones

Cuando algo no puede continuar, lanza una excepción con throw y captúrala donde sepas qué hacer, con try y catch. finally se ejecuta siempre.

En producción no muestres los errores en la página (pueden revelar datos internos): regístralos en un archivo.

Ejemplo práctico
<?php
function dividir(float $a, float $b): float
{
    if ($b == 0) {
        throw new InvalidArgumentException("No se puede dividir entre cero");
    }
    return $a / $b;
}

try {
    echo dividir(10, 0);
} catch (InvalidArgumentException $e) {
    echo "Error: " . $e->getMessage();
} finally {
    echo "\nCálculo terminado";
}

Ejercicio

Completa la palabra que lanza una excepción.

 new RuntimeException("Falta el archivo");
Ver la solución

Respuesta: throw. throw lanza la excepción, que sube hasta el catch más cercano.

Lección 11 · Avanzado

Funciones de arrays y funciones flecha

PHP trae muchas funciones para trabajar con arrays sin escribir bucles: array_filter se queda con los elementos que cumplen una condición, array_map transforma cada uno, array_sum los suma y usort ordena con el criterio que le indiques.

Las funciones flecha (fn($x) => $x * 2) son la forma corta de pasarles el criterio. A diferencia de las funciones anónimas con function, pueden usar las variables de fuera sin use.

Ejemplo práctico
<?php
$productos = [
    ['nombre' => 'Ratón', 'precio' => 12.5],
    ['nombre' => 'Teclado', 'precio' => 35],
    ['nombre' => 'Cable', 'precio' => 6],
];

$limite = 10;
$caros = array_filter($productos, fn($p) => $p['precio'] > $limite);
$nombres = array_map(fn($p) => mb_strtoupper($p['nombre']), $caros);
echo implode(', ', $nombres), "\n";                       // RATÓN, TECLADO

echo array_sum(array_column($productos, 'precio')), "\n";  // 53.5

usort($productos, fn($a, $b) => $b['precio'] <=> $a['precio']);
echo $productos[0]['nombre'], "\n";                       // Teclado

Ejercicio

¿Qué devuelve array_filter([1, 6, 3, 8], fn($n) => $n > 4)?

Ver la solución

Respuesta: [6, 8] con las claves 1 y 3 originales. array_filter conserva las claves originales ([1 => 6, 3 => 8]). Si necesitas reindexar desde 0, usa array_values() sobre el resultado.

Lección 12 · Avanzado

Fechas con DateTimeImmutable

DateTimeImmutable representa una fecha y hora que no cambia: métodos como modify('+30 days') devuelven un objeto nuevo y dejan el original igual, lo que evita errores difíciles de encontrar. Indica siempre la zona horaria (Europe/Madrid).

format('d/m/Y') la muestra con el formato que quieras y diff() calcula la diferencia entre dos fechas como un DateInterval con días, meses y años.

Ejemplo práctico
<?php
$zona = new DateTimeZone('Europe/Madrid');
$inicio = new DateTimeImmutable('2026-01-15', $zona);
$entrega = $inicio->modify('+30 days');

echo $entrega->format('d/m/Y'), "\n";        // 14/02/2026
echo $inicio->format('d/m/Y'), "\n";         // 15/01/2026: no ha cambiado

$fin = new DateTimeImmutable('2026-12-25', $zona);
$diferencia = $inicio->diff($fin);
echo $diferencia->days, " días\n";           // 344 días

$leida = DateTimeImmutable::createFromFormat('d/m/Y H:i', '04/05/2026 18:30', $zona);
echo $leida->format('l, H:i'), "\n";         // Monday, 18:30

Ejercicio

¿Qué pasa con $inicio después de $entrega = $inicio->modify('+30 days'); si es un DateTimeImmutable?

Ver la solución

Respuesta: No cambia: modify devuelve un objeto nuevo. Esa es la ventaja de la versión inmutable. Con DateTime (la mutable), modify cambiaría también $inicio.

Lección 13 · Avanzado

Sesiones: recordar al usuario entre páginas

HTTP no recuerda nada entre una petición y la siguiente. Las sesiones lo resuelven: session_start() crea (o recupera) una sesión, PHP guarda los datos en el servidor y el navegador solo conserva un identificador en una cookie.

Tras iniciar sesión, llama a session_regenerate_id(true) para cambiar ese identificador y evitar que alguien reutilice uno robado. Configura la cookie como httponly, secure y samesite para protegerla.

Ejemplo práctico
<?php
session_set_cookie_params([
    'httponly' => true,     // JavaScript no puede leerla
    'secure' => true,       // solo por HTTPS
    'samesite' => 'Lax',    // no se envía desde otras webs en peticiones peligrosas
]);
session_start();

// Al comprobar usuario y contraseña con éxito:
function iniciarSesion(int $idUsuario): void {
    session_regenerate_id(true);
    $_SESSION['usuario'] = $idUsuario;
}

// En cualquier página privada:
if (!isset($_SESSION['usuario'])) {
    header('Location: /login.php');
    exit;
}
echo "Hola, usuario {$_SESSION['usuario']}";

Ejercicio

¿Por qué se llama a session_regenerate_id(true) justo después de iniciar sesión?

Ver la solución

Respuesta: Para cambiar el identificador y que no sirva uno que alguien hubiera obtenido antes. Se llama «fijación de sesión»: si un atacante consigue que uses un identificador que él conoce, al iniciar sesión podría usarlo. Regenerarlo lo invalida.

Lección 14 · Avanzado

Enums y match

Desde PHP 8.1, enum define un tipo con un conjunto cerrado de valores, como los estados de un pedido. Si es un «backed enum» (enum Estado: string), cada caso tiene un valor que se puede guardar en la base de datos y recuperar con Estado::from().

match es como un switch que devuelve un valor, compara con === y lanza un error si ningún caso coincide, en vez de seguir en silencio.

Ejemplo práctico
<?php
enum Estado: string {
    case Pendiente = 'pendiente';
    case Enviado = 'enviado';
    case Entregado = 'entregado';

    public function etiqueta(): string {
        return match ($this) {
            Estado::Pendiente => 'Pendiente de envío',
            Estado::Enviado => 'En camino',
            Estado::Entregado => 'Entregado',
        };
    }
}

$estado = Estado::from('enviado');      // de la base de datos
echo $estado->etiqueta(), "\n";         // En camino
echo $estado->value, "\n";              // enviado
var_dump(Estado::tryFrom('perdido'));   // NULL: tryFrom no lanza error

Escribe el código

Escribe la función function tarifa(int $edad): string usando match (true): devuelve 'Infantil' si la edad es menor de 12, 'Mayor' si es 65 o más y 'General' en el resto de casos.

Tab escribe una sangría; para salir del editor con el teclado, pulsa Esc y luego Tab. Ctrl + Intro corrige.

Ver la solución
<?php
function tarifa(int $edad): string {
    return match (true) {
        $edad < 12 => 'Infantil',
        $edad >= 65 => 'Mayor',
        default => 'General',
    };
}

Con match (true), cada condición se compara con true y se usa la primera que se cumple. Sin default, una edad que no encaje lanzaría UnhandledMatchError.

Lección 15 · Avanzado

Contraseñas seguras con password_hash

Las contraseñas nunca se guardan tal cual ni con funciones rápidas como md5 o sha1, que se pueden probar por fuerza bruta a gran velocidad. password_hash calcula un hash lento y con sal (un valor aleatorio distinto para cada usuario) pensado para contraseñas.

Para comprobar una contraseña al iniciar sesión se usa password_verify, que compara de forma segura. password_needs_rehash indica si conviene recalcular el hash con un algoritmo o un coste más actuales.

Ejemplo práctico
<?php
// Al registrarse
$hash = password_hash('Mi contraseña larga 2026', PASSWORD_DEFAULT);
echo $hash, "\n";   // $2y$10$… (60 caracteres, distinto cada vez)

// Al iniciar sesión
$escrita = 'Mi contraseña larga 2026';
if (password_verify($escrita, $hash)) {
    echo "Contraseña correcta\n";
    if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
        $hash = password_hash($escrita, PASSWORD_DEFAULT);  // y guardarlo de nuevo
    }
} else {
    echo "Usuario o contraseña incorrectos\n";
}

Ejercicio

Completa para comprobar la contraseña escrita contra el hash guardado.

if (($escrita, $hash)) {
Ver la solución

Respuesta: password_verify. password_verify calcula el hash de lo escrito con la misma sal y lo compara con el guardado; nunca hace falta «descifrar» nada.

¿Has terminado? Sigue practicando

Repasa los conceptos en el diccionario, practica con estas herramientas o prueba otro lenguaje: las ideas que has aprendido (variables, condiciones, funciones, objetos…) te servirán en cualquiera.

  • Texto y programación

    Base64, URL y JSON

    Codifica y decodifica Base64 y URL, y valida o formatea JSON con la línea del error.

  • Seguridad y privacidad

    Analizar un enlace sospechoso

    Revisa un enlace sin abrirlo y descubre si imita a un banco o a otra web conocida.

  • Texto y programación

    Selector de colores y contraste

    Convierte colores entre HEX, RGB y HSL, toma colores de una imagen y comprueba el contraste WCAG.

Fuentes consultadas

* Porcentaje de personas que dijeron haber usado PHP en la Stack Overflow Developer Survey 2026: Technology (pregunta sobre lenguajes de programación, de scripts y de marcado; todas las respuestas). El progreso de las lecciones se guarda solo en este navegador.