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
1
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.
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.
Los errores
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) {
Con una edad de 10, el script con errores diría «Casi: te falta un año» y además cambiaría $edad a 17.
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) {
4
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.
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.
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
5
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).
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.
Una solución posible
<?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.
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.
6
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).
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.
7
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.
Los errores
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();
Hacer privadas las propiedades y ofrecer métodos para usarlas impide que otro código deje el objeto en un estado incorrecto.
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
8
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.
9
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.
10
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.
11
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.
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.
12
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.
13
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.
14
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.
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.
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.
15
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.
* 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.
¿Nos dejas medir las visitas?
Con tu permiso usamos Google Analytics, que guarda cookies, para saber cuántas personas visitan la web y qué páginas les resultan útiles. No usamos cookies de publicidad y tus archivos nunca salen de tu navegador. Puedes cambiar de opinión cuando quieras en «Configurar cookies», al pie de la página. Más información.