UT02 · Introducción a PHP: de los primeros programas a una aplicación web
Del primer script a una aplicación web con estado: entrada HTTP, built-ins, funciones propias, objetos, persistencia ligera, autenticación, ficheros seguros y notificaciones por correo.
☰ Contenido de la unidad
Qué aprenderás
Una página HTML es un documento que el navegador interpreta y muestra. Una aplicación web puede necesitar calcular un precio, escoger qué contenido mostrar, procesar los datos de un formulario o recordar qué usuario ha iniciado sesión. PHP permite desarrollar esa lógica en el servidor.
En esta unidad aprenderemos PHP desde sus elementos más sencillos. No es necesario haber programado antes en PHP; sí conviene recordar la estructura básica de HTML y los atributos name, action y method de los formularios. Comenzaremos con valores escritos directamente en el código; después aprenderemos a recibir información, primero desde la terminal como contraste y, sobre todo, mediante formularios HTTP. A continuación reutilizaremos esa lógica mediante funciones y, más adelante, mediante métodos de objetos sencillos.
Resultado de aprendizaje relacionado: RA5 — Genera documentos web utilizando lenguajes de guiones de servidor. Los criterios de evaluación vinculados a la unidad se identifican en el frontmatter. La programación orientada a objetos se introduce aquí como una herramienta para comprender bibliotecas y aplicaciones PHP; no supone un resultado de aprendizaje adicional.

Método de trabajo. En cada apartado encontraremos una pregunta inicial, un ejemplo corto explicado línea a línea, una prueba de escritorio (predecir el resultado), ejercicios graduados y una comprobación de lo aprendido. Una vez dominados esos elementos los utilizaremos juntos para desarrollar una aplicación sencilla de gestión de incidencias, IAW Desk.
Distingue dos cosas: aprender la sintaxis de PHP y aprender a implantar una aplicación PHP. En IAW necesitamos ambas, pero avanzaremos de la primera a la segunda de manera gradual.
Puente hacia el Proyecto 1
Esta UT prepara varias piezas que después serán obligatorias en el proyecto PHP:
| Necesidad del proyecto | Dónde se prepara |
|---|---|
| Recibir, normalizar y validar datos | formularios y funciones incorporadas |
| Organizar lógica reutilizable | funciones propias y modularidad |
| Autenticación, roles y estado | cookies, sesiones y autorización |
| Protección CSRF | sesión + token + hash_equals() |
| Subida segura de ficheros | $_FILES, MIME real, tamaño y renombrado |
| Logs y diagnóstico | ficheros, error_log() y separación usuario/técnico |
| Correo electrónico | PHPMailer como integración complementaria |
Todavía no implementamos aquí todo el Proyecto 1. PDO, CRUD persistente, búsqueda/paginación real, transacciones y auditoría vinculada a la base de datos se consolidarán en la UT03 con MariaDB. Esta separación evita mezclar demasiadas capas antes de dominar cada una.
1. Dónde y cómo ejecutamos PHP
1.1. Cliente, servidor y lenguaje del lado servidor
Imagina que escribimos en el navegador la dirección de una página llamada hola.php. El navegador no interpreta las instrucciones PHP. Realiza una petición HTTP al servidor web; allí se ejecuta PHP, se genera una respuesta y el navegador muestra el contenido recibido.

| Elemento | Responsabilidad |
|---|---|
| Navegador | Solicita la página y representa el HTML recibido. |
| Apache | Recibe la petición HTTP y selecciona el sitio o recurso solicitado. |
| PHP-FPM | Ejecuta el programa PHP cuando Apache le deriva la petición. |
Archivo .php |
Contiene las instrucciones que escribimos. Está almacenado en el servidor. |
| HTML resultante | Es el contenido que recibirá el navegador. |
JavaScript ejecutado en el navegador y PHP ejecutado en el servidor pueden colaborar, pero no son equivalentes. El código PHP no debería enviarse al cliente como texto. Si abres una dirección HTTP y se muestran las etiquetas <?php literalmente, detén la práctica: hay un problema en la configuración del servidor.
1.2. Cómo trabajamos con VS Code
El entorno habitual del laboratorio separa el equipo desde el que desarrollamos y el servidor que ejecuta nuestra aplicación:
Equipo del alumno (Windows o Linux con escritorio)
├── VS Code + extensión Remote - SSH
└── Navegador web
│
├── SSH ───> Debian: editar los ficheros PHP
└── HTTP ──> Apache: solicitar los resultados
Debian de trabajo (IAW-WEB01)
├── OpenSSH
├── Apache
├── PHP-FPM
└── /srv/iaw/php/public/ ← archivos publicados
Remote - SSH abre en VS Code una carpeta que está realmente en Debian. Guardar en el editor modifica el archivo del servidor; no es necesario subirlo manualmente mediante FTP después de cada cambio.
Antes de utilizar la extensión verifica la conexión SSH desde el equipo cliente. Sustituye alumno por tu usuario y la IP por la que tenga tu máquina Debian:
ssh alumno@192.168.60.10
Después, en VS Code, utiliza Ctrl + Shift + P → Remote-SSH: Connect to Host… → alumno@192.168.60.10 → File > Open Folder. El servidor remoto necesita OpenSSH y los requisitos habituales de VS Code Server. La documentación oficial de Remote - SSH explica el procedimiento detallado.
1.3. Carpeta pública y código interno
Usaremos una estructura sencilla que podrá ampliarse posteriormente:
/srv/iaw/php/
├── public/ ← DocumentRoot del sitio Apache
│ ├── 01_hola.php
│ └── index.php
├── src/ ← funciones y clases reutilizables
└── templates/ ← fragmentos HTML/PHP para presentación
Solo public/ debe quedar expuesto a HTTP. La carpeta src/ contiene código que se carga desde otros archivos PHP, pero no se consulta directamente desde el navegador. El VirtualHost y la integración de Apache con PHP-FPM se estudian y preparan en la UT01.
En un laboratorio donde el servidor ya está configurado, las comprobaciones más útiles son:
php -v # Versión de PHP disponible en la terminal
php -m # Extensiones cargadas para PHP CLI
systemctl status apache2
systemctl status php8.4-fpm # En Debian 13 con el paquete PHP 8.4
php -v describe el intérprete de la terminal (CLI); no basta por sí solo para demostrar que Apache y PHP-FPM estén funcionando. El nombre concreto del servicio FPM debe comprobarse según la versión instalada. Un php -S localhost:8000 -t public permite hacer una prueba local controlada, pero el servidor integrado de PHP está destinado a desarrollo y demostraciones, no a producción.
Para comenzar a programar, el laboratorio debe estar preparado. No confundas un error de SSH, de Apache, de permisos o de PHP-FPM con un error de sintaxis del programa que estás aprendiendo a escribir.
2. Tu primer programa PHP
2.1. Una instrucción, un resultado
Crea el archivo public/01_hola.php y escribe exactamente:
<?php
echo 'Hola desde PHP';
Qué significa cada elemento:
| Elemento | Explicación |
|---|---|
<?php |
Indica dónde comienzan las instrucciones PHP. |
echo |
Envía texto al resultado que se va a mostrar. |
'Hola desde PHP' |
Es una cadena de texto o string. |
; |
Finaliza esta instrucción. |
Guarda el archivo. En la terminal remota, desde la raíz de tu proyecto, comprueba la sintaxis:
php -l public/01_hola.php
Después ábrelo mediante la URL HTTP del sitio que publicó Apache, por ejemplo http://php07.iaw.test/01_hola.php si ese nombre está configurado para tu laboratorio. No utilices file:///…/01_hola.php: abrir un fichero local directamente en el navegador no ejecuta PHP.
El resultado visible será:
Hola desde PHP
Si aparece una página en blanco o un mensaje de error, no avances todavía: comprueba que has guardado el archivo, que la URL corresponde al DocumentRoot correcto y que php -l no comunica errores de sintaxis.
2.2. Varias instrucciones y comentarios
<?php
// Primera línea de salida.
echo 'Buenos días';
echo '<br>';
/*
Este comentario puede ocupar
varias líneas.
*/
echo 'Estoy aprendiendo PHP';
echo '<br>'; escribe una etiqueta HTML para que, al interpretarse como HTML en el navegador, el siguiente texto aparezca en otra línea. Si ejecutas el mismo archivo con php public/01_hola.php desde la terminal, verás texto y etiquetas: la terminal no representa HTML como un navegador.
Prueba de escritorio: antes de ejecutar, escribe qué tres fragmentos emitirá PHP y cuáles no aparecerán por ser comentarios.
2.3. HTML y PHP en un mismo fichero
La extensión .php permite escribir HTML normal junto a pequeños fragmentos ejecutados en el servidor:
<!doctype html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Mi primera página PHP</title>
</head>
<body>
<h1>Mi primera página dinámica</h1>
<p><?php echo 'Generado en el servidor'; ?></p>
</body>
</html>
El documento completo sigue siendo HTML. La etiqueta <?php cambia temporalmente a código PHP y ?> permite volver al HTML. Si el archivo contiene solo código PHP, habitualmente se omite el cierre final ?> para evitar generar espacios o líneas en blanco accidentalmente.
2.4. Qué ocurre cuando hay un error
Prueba intencionadamente este fragmento en una copia:
<?php
echo 'Texto sin terminar'
Ejecuta php -l archivo.php. La herramienta señalará el problema; falta ; al terminar la instrucción. Corrígelo, vuelve a ejecutar el comprobador y solo después consulta el navegador.
Ejercicios 2.1–2.5. Crea un saludo con tu nombre y número de puesto; genera tres párrafos; cambia el título HTML; explica por qué la terminal muestra <p> mientras el navegador lo representa; provoca y corrige un error por falta de ;. El objetivo es distinguir editor, intérprete y navegador.
3. Variables, asignación y tipos
3.1. ¿Por qué necesitamos variables?
Si necesitamos calcular el precio de distintas intervenciones, escribir siempre un número fijo dentro de la fórmula nos obliga a modificar el programa cada vez. Una variable permite asignar un nombre a un valor y reutilizarlo.
<?php
$horas = 3;
$tarifa = 20;
$total = $horas * $tarifa;
echo $total;
Lectura línea a línea:
$horas = 3;guarda el valor3en la variablehoras.$tarifa = 20;guarda20en otra variable.$total = $horas * $tarifa;multiplica los valores y almacena60.echo $total;produce el resultado60.
La instrucción = se llama asignación: calcula el lado derecho y guarda el resultado en la variable del lado izquierdo. No equivale a plantear una igualdad matemática.

3.2. Reasignación: el valor puede cambiar
<?php
$horas = 3;
$horas = 4;
echo $horas; // 4
En el segundo paso se sustituye el valor anterior. El mismo razonamiento permite aumentar un contador:
<?php
$visitas = 1;
$visitas = $visitas + 1;
echo $visitas; // 2
| Momento | Instrucción | Valor de $visitas |
|---|---|---|
| Inicial | $visitas = 1; |
1 |
| Actualización | $visitas = $visitas + 1; |
2 |
| Visualización | echo $visitas; |
2 |
Las variables se escriben con $ y distinguen mayúsculas y minúsculas. $nombre y $Nombre no son la misma variable. Es preferible utilizar nombres expresivos ($precioHora, $totalHoras) en lugar de $x o $dato1 cuando el significado no está claro.
3.3. Tipos de datos fundamentales
| Tipo | Qué representa | Ejemplo PHP |
|---|---|---|
int |
Entero sin parte decimal | $puesto = 7; |
float |
Número con parte decimal | $tarifa = 18.50; |
string |
Texto | $nombre = 'Ana'; |
bool |
Verdadero o falso | $activo = true; |
null |
Ausencia de valor | $responsable = null; |
<?php
$puesto = 7;
$tarifa = 18.50;
$nombre = 'Ana';
$urgente = false;
$responsable = null;
var_dump($puesto);
var_dump($tarifa);
var_dump($nombre);
var_dump($urgente);
var_dump($responsable);
var_dump sirve para observar el tipo y el contenido durante el aprendizaje y la depuración. No suele ser la presentación definitiva de una aplicación.
Ojo con bool: echo false; no escribe la palabra false. Para estudiar un valor booleano, utiliza var_dump y comprueba qué significa cada resultado.
3.4. Variables frente a constantes
Una variable puede cambiar. Una constante representa un valor que no queremos reasignar durante la ejecución:
<?php
const IVA = 0.21;
$base = 100;
$total = $base * (1 + IVA);
echo $total; // 121
IVA no lleva $. PHP permite declarar constantes mediante const y también mediante define(). Por el momento utilizaremos const cuando el valor sea fijo y conocido al escribir el programa.
Ejercicios 3.1–3.6. Muestra tus datos utilizando variables; intercambia los valores de dos variables con ayuda de una tercera; calcula un salario; calcula la media de tres notas; crea un programa que convierta minutos a segundos; predice la salida tras tres reasignaciones. Todos los datos estarán escritos inicialmente en el propio programa, sin formularios ni lectura «por teclado».
4. Cadenas y operadores, con ejemplos
Una expresión es una combinación de valores, variables y operadores que PHP puede calcular. No es suficiente memorizar una tabla de símbolos: debemos entender qué hace cada uno, qué resultado produce y de qué tipo es ese resultado.
4.1. Comillas simples, dobles y concatenación
Las cadenas de texto se delimitan normalmente con comillas simples o dobles:
<?php
$nombre = 'Ana';
echo 'Hola $nombre'; // Muestra literalmente: Hola $nombre
echo "Hola $nombre"; // Muestra: Hola Ana
En cadenas delimitadas por comillas dobles PHP puede interpolar variables sencillas. En cadenas delimitadas por comillas simples, $nombre se conserva como texto literal.
También podemos concatenar: unir textos mediante el punto ..
<?php
$nombre = 'Ana';
$apellido = 'García';
$completo = $nombre . ' ' . $apellido;
echo $completo; // Ana García
Presta atención al espacio ' ': sin él el resultado sería AnaGarcía.
| Expresión | Resultado | Explicación |
|---|---|---|
'IAW' . '02' |
IAW02 |
Se unen cadenas. |
2 + 3 |
5 |
Se suman números. |
'2' . '3' |
23 |
Los dígitos son texto y se concatenan. |
'Hola ' . $nombre |
Hola Ana |
Se une texto fijo y una variable. |
Comprueba la diferencia: echo '2' . '3'; no equivale a echo 2 + 3;.
4.2. Operadores aritméticos
| Operador | Operación | Ejemplo | Resultado |
|---|---|---|---|
+ |
Suma | 7 + 3 |
10 |
- |
Resta | 7 - 3 |
4 |
* |
Multiplicación | 7 * 3 |
21 |
/ |
División | 7 / 2 |
3.5 |
% |
Resto de división entera | 7 % 3 |
1 |
** |
Potencia | 2 ** 3 |
8 |
Prueba los operadores sin formularios:
<?php
$a = 7;
$b = 3;
echo $a + $b; // 10
echo '<br>';
echo $a - $b; // 4
echo '<br>';
echo $a * $b; // 21
echo '<br>';
echo $a / $b; // 2.333...
echo '<br>';
echo $a % $b; // 1
El resto % merece una prueba propia. 10 % 2 es 0, porque 10 es divisible por 2; 11 % 2 es 1, porque sobra una unidad. Más adelante lo utilizaremos para detectar números pares e impares.
División entre cero. No ejecutaremos una división si el divisor es cero. Cuando estudiemos condiciones veremos cómo impedirla con if.
4.3. Orden de las operaciones
PHP aplica reglas de precedencia, como en matemáticas. Los paréntesis ayudan a indicar con claridad la operación deseada:
<?php
echo 2 + 3 * 4; // 14
echo '<br>';
echo (2 + 3) * 4; // 20
Cálculo guiado de una factura:
<?php
$precio = 25;
$unidades = 3;
$descuento = 5;
$subtotal = $precio * $unidades; // 75
$base = $subtotal - $descuento; // 70
$total = $base * 1.21; // 84.7
echo number_format($total, 2, ',', '.') . ' €'; // 84,70 €
number_format da formato para mostrar el número; no es el mecanismo con el que se garantiza la precisión de cálculos monetarios complejos. Para nuestros ejercicios iniciales utilizaremos importes sencillos.
4.4. Operadores de asignación abreviada
Ya conocemos =. Algunos operadores permiten escribir de manera más breve una actualización:
| Forma larga | Forma abreviada | Efecto |
|---|---|---|
$n = $n + 2; |
$n += 2; |
Suma 2 al valor anterior. |
$n = $n - 2; |
$n -= 2; |
Resta 2. |
$n = $n * 2; |
$n *= 2; |
Multiplica por 2. |
$n = $n / 2; |
$n /= 2; |
Divide entre 2. |
$texto = $texto . '!'; |
$texto .= '!'; |
Añade texto. |
<?php
$puntos = 10;
$puntos += 5; // 15
$puntos -= 3; // 12
$puntos *= 2; // 24
echo $puntos; // 24
Los incrementos $n++ y $n-- aumentan o disminuyen una unidad. Por ahora los usaremos como instrucciones independientes, sin mezclar su resultado con otras expresiones:
<?php
$contador = 0;
$contador++;
$contador++;
echo $contador; // 2
4.5. Operadores de comparación: producen true o false
Una comparación responde a una pregunta. No guarda un valor ni muestra automáticamente un mensaje. Produce un booleano que después podrá usar if.
| Operador | Pregunta | Ejemplo | Resultado |
|---|---|---|---|
=== |
¿Mismo valor y mismo tipo? | 7 === 7 |
true |
!== |
¿No son idénticos? | 7 !== 8 |
true |
> |
¿Es mayor? | 7 > 8 |
false |
< |
¿Es menor? | 7 < 8 |
true |
>= |
¿Mayor o igual? | 7 >= 7 |
true |
<= |
¿Menor o igual? | 5 <= 3 |
false |
Existe además == (igualdad no estricta). Puede convertir tipos antes de comparar, por lo que conviene saber distinguirla de ===:
<?php
$numero = 7;
$texto = '7';
var_dump($numero == $texto); // bool(true)
var_dump($numero === $texto); // bool(false)
El contenido representa el mismo número, pero uno es entero y otro es texto. En estos apuntes utilizaremos preferentemente comparación estricta cuando importe distinguir valores y tipos. = es asignación; == y === son comparaciones.
4.6. Operadores lógicos: AND, OR y NOT
Los operadores relacionales anteriores permiten formular preguntas sencillas: «¿la nota es al menos 5?», «¿la temperatura supera 35?» o «¿dos valores son idénticos?». Muchas decisiones reales necesitan combinar varias preguntas. Para eso utilizamos operadores lógicos.
| Idea lógica | Operador que usaremos | También existe | Resultado verdadero cuando… |
|---|---|---|---|
| AND (Y) | && |
and |
Todas las condiciones combinadas son verdaderas. |
| OR (O) | || |
or |
Al menos una de las condiciones es verdadera. |
| NOT (NO) | ! |
— | Invierte un booleano: true pasa a false y viceversa. |
En estos apuntes utilizaremos normalmente &&, || y !. PHP también permite escribir and y or, pero no tienen la misma precedencia que && y || cuando se mezclan con otras operaciones, especialmente con asignaciones. Para empezar es más seguro mantener una única forma y usar paréntesis cuando una expresión pueda resultar ambigua.
AND: todas las condiciones deben cumplirse
<?php
$edad = 19;
$autorizado = true;
var_dump($edad >= 18); // bool(true)
var_dump($autorizado === true); // bool(true)
var_dump($edad >= 18 && $autorizado === true); // bool(true)
La última expresión puede leerse así: «la edad es al menos 18 Y además está autorizado». Si cualquiera de las dos partes fuese falsa, el resultado completo sería false.
| Primera condición | Segunda condición | && |
|---|---|---|
true |
true |
true |
true |
false |
false |
false |
true |
false |
false |
false |
false |
OR: basta con que se cumpla una condición
<?php
$prioridadAlta = true;
$servicioCaido = false;
var_dump($prioridadAlta || $servicioCaido); // bool(true)
Se lee: «la prioridad es alta O el servicio está caído». Como la primera parte ya es verdadera, toda la expresión resulta verdadera.
| Primera condición | Segunda condición | || |
|---|---|---|
true |
true |
true |
true |
false |
true |
false |
true |
true |
false |
false |
false |
NOT: invertir una condición
<?php
$activo = false;
var_dump($activo); // bool(false)
var_dump(!$activo); // bool(true)
!$activo significa «NO está activo». Conviene que el nombre de las variables booleanas permita leer la condición de forma natural.
4.7. Precedencia lógica y uso de paréntesis
Cuando una expresión contiene varios operadores, PHP no los evalúa simplemente de izquierda a derecha. Cada operador tiene una precedencia. Para las expresiones que estamos aprendiendo, recuerda esta simplificación útil:
- Los paréntesis
()fuerzan primero la evaluación de su contenido. !se aplica antes que&&y||.- Las comparaciones como
===,>,<,>=o<=producen los booleanos que después combinaremos. &&se evalúa antes que||.
Importante: los corchetes
[]no se utilizan para agrupar condiciones. Más adelante los usaremos para acceder a posiciones o claves de arrays. Para controlar el orden lógico utilizamos paréntesis().
Compara estos dos casos:
<?php
var_dump(true || false && false); // bool(true)
var_dump((true || false) && false); // bool(false)
En la primera expresión, && tiene prioridad sobre ||, por lo que PHP interpreta la operación de forma equivalente a:
true || (false && false)
true || false
true
En la segunda expresión, los paréntesis obligan a resolver primero true || false:
(true || false) && false
true && false
false
Ejemplo profesional: una regla de acceso
Supongamos que una operación requiere que la cuenta esté activa y que el usuario tenga uno de dos perfiles permitidos. Todavía no necesitamos arrays ni sesiones para razonar la condición; utilizaremos valores fijos:
<?php
$activo = true;
$esAdministrador = false;
$esTecnico = true;
$puedeAcceder = $activo && ($esAdministrador || $esTecnico);
var_dump($puedeAcceder); // bool(true)
Los paréntesis expresan exactamente la regla: debe estar activo Y, además, ser administrador O técnico. Sin esos paréntesis, la lectura sería menos clara y podría no representar la intención de negocio.
Otro ejemplo: negar un grupo completo
<?php
$esFestivo = false;
$esFinDeSemana = true;
$esLaborable = !($esFestivo || $esFinDeSemana);
var_dump($esLaborable); // bool(false)
Primero se calcula si es festivo o fin de semana; después ! invierte ese resultado. No equivale a negar solo una de las dos condiciones.
&& / || frente a and / or
Observa esta diferencia, que explica por qué usaremos preferentemente los operadores simbólicos:
<?php
$a = true && false;
$b = true and false;
var_dump($a); // bool(false)
var_dump($b); // bool(true)
La segunda línea sorprende porque = tiene mayor precedencia que and: primero se asigna true a $b y después se evalúa el and. No necesitamos memorizar toda la tabla de precedencias; sí necesitamos saber que mezclar formas sin comprenderlas puede cambiar el resultado. En nuestras condiciones utilizaremos &&, ||, ! y paréntesis explícitos.
Cortocircuito. Con &&, si la primera parte ya es falsa, PHP no necesita evaluar la segunda para saber que el resultado será falso. Con ||, si la primera parte ya es verdadera, tampoco necesita evaluar la segunda. Esta propiedad será útil más adelante para comprobar valores antes de realizar otras operaciones.
Ejercicios 4.10–4.15. Predice y comprueba las cuatro combinaciones posibles de &&; repite con ||; invierte cinco booleanos con !; determina el resultado de true || false && false antes de ejecutarlo; añade paréntesis para obtener el resultado contrario; expresa con variables booleanas la regla «activo y (administrador o técnico)» y prueba al menos tres combinaciones distintas.
Ejercicios 4.1–4.9. Calcula el área de rectángulo y círculo; transforma Celsius a Fahrenheit; calcula el porcentaje de ocupación de un aula; convierte segundos a horas/minutos/segundos; calcula el importe de horas extraordinarias partiendo de valores fijos; demuestra la diferencia entre concatenar y sumar; predice cinco comparaciones con var_dump; modifica una factura mediante += y -=; comprueba qué ocurre con / y % en casos distintos de cero.
5. Decisiones: if, else y elseif, sin saltarnos pasos
Hasta ahora los programas han ejecutado las instrucciones en el orden en el que están escritas. Pero una aplicación debe tomar decisiones: ¿hay que mostrar una alerta? ¿la prioridad es alta? ¿el divisor es válido?

5.1. Primero, un if simple
Lee la frase: «Si la nota es al menos 5, muestra Aprobado». No se ha indicado qué hacer si la nota es menor. Esa es la función del if simple.
<?php
$nota = 7;
if ($nota >= 5) {
echo 'Aprobado';
}
| Elemento | Significado |
|---|---|
if |
Introduce una condición. |
($nota >= 5) |
Pregunta si la nota es mayor o igual que cinco. |
{ y } |
Delimitan el bloque que se ejecutará cuando sea verdad. |
echo 'Aprobado'; |
Solo se ejecuta si la condición resulta true. |
Prueba dos valores: con $nota = 7 aparece Aprobado; con $nota = 3 no aparece ese texto. El programa no tiene por qué mostrar un mensaje si la condición resulta falsa.
Otro if simple:
<?php
$temperatura = 39;
if ($temperatura > 35) {
echo 'Aviso: temperatura elevada';
}
5.2. Después, if con else: dos caminos
Ahora la regla sí exige un resultado para el otro caso: «Si la nota es al menos 5, aprobado; en caso contrario, suspenso».
<?php
$nota = 3;
if ($nota >= 5) {
echo 'Aprobado';
} else {
echo 'Suspenso';
}
¿Cómo se lee? Se evalúa una sola vez $nota >= 5. Si es verdadera se ejecuta el primer bloque. Si es falsa se ejecuta el segundo. No se ejecutan ambos bloques en una misma evaluación.
$nota |
¿$nota >= 5? |
Salida |
|---|---|---|
| 3 | false |
Suspenso |
| 5 | true |
Aprobado |
| 9 | true |
Aprobado |
5.3. elseif: tres o más alternativas excluyentes
Para distinguir suspenso, aprobado y notable/sobresaliente, necesitamos probar otra condición cuando la anterior sea falsa.
<?php
$nota = 8;
if ($nota < 5) {
echo 'Suspenso';
} elseif ($nota < 7) {
echo 'Aprobado';
} else {
echo 'Notable o sobresaliente';
}
Con $nota = 8, primero se comprueba 8 < 5 (falso); después 8 < 7 (falso); finalmente se ejecuta else. Con nota 6, la primera comparación es falsa y la segunda verdadera: no se llega a else.
Prueba de escritorio: repite el recorrido para 4, 5, 6, 7 y 10 antes de ejecutar. Anota qué condiciones se consultan realmente en cada caso.
5.4. Solo ahora: un if anidado
Anidar significa poner una condición dentro del bloque de otra. Primero debe cumplirse la condición externa; después se evalúa la interna.
<?php
$usuarioActivo = true;
$esAdministrador = false;
if ($usuarioActivo) {
echo 'Usuario activo. ';
if ($esAdministrador) {
echo 'Puede administrar.';
} else {
echo 'Puede consultar.';
}
} else {
echo 'Acceso deshabilitado.';
}
Para entenderlo, sigue los niveles:
- ¿Está activa la cuenta?
- Solo si lo está, ¿tiene permisos de administración?
- La respuesta interna determina el mensaje final.
En este ejemplo, también podría utilizarse una condición lógica para un resultado más sencillo. Los anidamientos son útiles cuando queremos realizar varias acciones dentro de un caso, pero demasiados niveles hacen difícil comprender el programa.
5.5. Comprobación práctica: evitar dividir entre cero
<?php
$dividendo = 12;
$divisor = 0;
if ($divisor === 0) {
echo 'No se puede dividir entre cero';
} else {
echo $dividendo / $divisor;
}
No se ejecuta la división si el divisor vale cero. Esta comprobación no exige todavía formularios ni funciones: estamos aprendiendo a decidir con valores previamente definidos.
5.6. switch y match: opciones para más adelante
Cuando una variable puede tomar varios valores concretos, switch permite agrupar casos:
<?php
$prioridad = 'media';
switch ($prioridad) {
case 'alta':
echo 'Atención inmediata';
break;
case 'media':
echo 'Planificar intervención';
break;
default:
echo 'Revisión ordinaria';
}
break termina el switch cuando se ha resuelto un caso. Más adelante podrás reconocer match, que devuelve un valor y compara de forma estricta:
<?php
$prioridad = 'alta';
$mensaje = match ($prioridad) {
'alta' => 'Hoy',
'media' => 'Esta semana',
default => 'Sin urgencia',
};
echo $mensaje;
No es necesario introducir switch y match antes de dominar if simple → if/else → elseif.
Ejercicios 5.1–5.11. Determina si un número es positivo; comprueba si es par; muestra aprobado/suspenso; clasifica una nota en tres tramos; encuentra el mayor de dos números; encuentra el mayor de tres; calcula el salario con recargo de horas extraordinarias; clasifica un ticket por prioridad; comprueba si una cuenta está activa; valida que una nota esté entre 0 y 10; resuelve una calculadora con división protegida. Para cada ejercicio prueba al menos un caso verdadero, uno falso y un límite.
6. Repetir instrucciones: los bucles
Hasta ahora hemos elegido qué código ejecutar. En esta sección aprenderemos cuántas veces repetirlo. Antes de utilizar arrays, practicaremos con un simple contador.

6.1. ¿Por qué no escribir diez veces lo mismo?
<?php
echo '1';
echo '2';
echo '3';
Para tres números podemos escribir tres instrucciones; para cien o diez mil es poco práctico. Un bucle ejecuta repetidamente un bloque mientras se cumpla una regla.
6.2. Bucle for: sabemos las vueltas que queremos hacer
<?php
for ($i = 1; $i <= 5; $i++) {
echo $i . '<br>';
}
El for contiene tres partes:
| Parte | Fragmento | Significado |
|---|---|---|
| Inicialización | $i = 1 |
Empieza contando en 1. |
| Condición | $i <= 5 |
Repite mientras sea verdadera. |
| Actualización | $i++ |
Suma 1 después de cada vuelta. |
Traza del programa:
| Vuelta | $i al comprobar |
¿$i <= 5? |
¿Qué escribe? |
|---|---|---|---|
| 1 | 1 | Sí | 1 |
| 2 | 2 | Sí | 2 |
| 3 | 3 | Sí | 3 |
| 4 | 4 | Sí | 4 |
| 5 | 5 | Sí | 5 |
| — | 6 | No | Nada; termina |
Observa que el último valor comprobado puede no llegar a mostrarse. El bucle termina cuando la condición pasa a ser falsa.
Tabla de multiplicar:
<?php
$numero = 7;
for ($i = 1; $i <= 10; $i++) {
$producto = $numero * $i;
echo $numero . ' × ' . $i . ' = ' . $producto . '<br>';
}
6.3. Bucle while: repetimos mientras se cumpla una condición
Un while comprueba la condición antes de entrar al bloque:
<?php
$contador = 1;
while ($contador <= 5) {
echo $contador . '<br>';
$contador++;
}
Comparación con el for: la inicialización está fuera del bucle y la actualización está dentro. Si olvidamos $contador++, la condición nunca dejará de cumplirse y aparecerá un bucle infinito.
6.4. do ... while: al menos una ejecución
Este bucle comprueba al final:
<?php
$contador = 8;
do {
echo $contador;
$contador++;
} while ($contador <= 5);
Aunque 8 no cumple la condición <= 5, el cuerpo ya se ha ejecutado una vez. Este es el rasgo que diferencia do ... while de while.
6.5. Contadores y acumuladores
Un contador suele aumentar una unidad por cada caso. Un acumulador suma valores para obtener un total.
<?php
$suma = 0;
for ($i = 1; $i <= 4; $i++) {
$suma += $i;
}
echo $suma; // 10
| Vuelta | $i |
$suma antes |
$suma después |
|---|---|---|---|
| 1 | 1 | 0 | 1 |
| 2 | 2 | 1 | 3 |
| 3 | 3 | 3 | 6 |
| 4 | 4 | 6 | 10 |
La variable $suma debe inicializarse antes del bucle. Si la reiniciamos a cero dentro de cada vuelta perderemos lo acumulado.
6.6. break y continue
break termina el bucle. continue salta el resto de la vuelta actual y continúa con la siguiente:
<?php
for ($i = 1; $i <= 6; $i++) {
if ($i === 3) {
continue;
}
if ($i === 6) {
break;
}
echo $i . ' ';
}
Salida: 1 2 4 5 . El 3 se omite y el 6 provoca la salida antes de imprimirse.
Ejercicios 6.1–6.11. Muestra 1…20; cuenta de 10 a 1; escribe solo los pares; genera la tabla de multiplicar de tu puesto; suma 1…N; calcula un factorial iterativo; encuentra múltiplos de 3 hasta N; suma los cuadrados de los diez primeros naturales; calcula la media de cinco notas fijadas en el programa; identifica y corrige un bucle infinito; imprime los números de un rango sin mostrar un valor prohibido. No empieces con 10 000 filas en el navegador: prueba primero un rango pequeño.
7. Arrays y foreach: trabajar con varios datos
Ahora sabemos almacenar un valor, elegir un camino y repetir instrucciones. Podemos dar el siguiente paso: almacenar varios valores relacionados en una misma estructura.
7.1. El problema de utilizar muchas variables
Si necesitamos guardar tres prioridades, podríamos escribir:
<?php
$prioridad1 = 'baja';
$prioridad2 = 'media';
$prioridad3 = 'alta';
Pero ¿qué pasaría si tuviéramos cien? Un array es una colección de valores que podemos guardar bajo un mismo nombre.
7.2. Array indexado: posiciones que empiezan en cero
<?php
$prioridades = ['baja', 'media', 'alta'];
echo $prioridades[0]; // baja
echo $prioridades[1]; // media
echo $prioridades[2]; // alta

| Posición | Valor |
|---|---|
0 |
baja |
1 |
media |
2 |
alta |
El primer elemento se encuentra en la posición 0, no en la posición 1. count($prioridades) devuelve 3: el número de elementos, no el último índice.
Podemos modificar o añadir elementos:
<?php
$prioridades = ['baja', 'media', 'alta'];
$prioridades[1] = 'normal';
$prioridades[] = 'crítica';
var_dump($prioridades);
No confundas $prioridades[1] = 'normal' (modifica la posición 1) con $prioridades[] = 'crítica' (añade un elemento al final).
7.3. Recorrer un array utilizando for
Como ya conocemos for, podemos utilizarlo para mostrar cada posición:
<?php
$prioridades = ['baja', 'media', 'alta'];
for ($i = 0; $i < count($prioridades); $i++) {
echo $prioridades[$i] . '<br>';
}
Lee el recorrido: $i vale 0, después 1, después 2. Al llegar a 3, la condición 3 < 3 resulta falsa y el bucle termina.
Esta construcción funciona para un array que tenga índices consecutivos de 0 a count()-1. No debemos dar por hecho que todos los arrays conservarán siempre esa forma si se eliminan elementos o se utilizan claves personalizadas.
7.4. foreach: recorrer sin administrar manualmente los índices
PHP dispone de una estructura especialmente práctica para colecciones:
<?php
$prioridades = ['baja', 'media', 'alta'];
foreach ($prioridades as $prioridad) {
echo $prioridad . '<br>';
}
Se lee: «Por cada elemento de $prioridades, toma su valor en $prioridad y ejecuta el bloque». No hemos tenido que escribir $i = 0 ni actualizar $i++.
| Vuelta | $prioridad |
|---|---|
| 1 | baja |
| 2 | media |
| 3 | alta |
foreach trabaja igualmente si el array está vacío: en ese caso, el cuerpo no se ejecuta ninguna vez.
7.5. Array asociativo: claves con significado
A veces una posición numérica no basta para entender el contenido. Un ticket puede tener codigo, asunto y prioridad. Un array asociativo utiliza claves que describen cada dato:
<?php
$ticket = [
'codigo' => 'IAW-07',
'asunto' => 'No carga la página',
'prioridad' => 'alta',
];
echo $ticket['asunto']; // No carga la página
| Clave | Valor |
|---|---|
'codigo' |
'IAW-07' |
'asunto' |
'No carga la página' |
'prioridad' |
'alta' |
El símbolo => asocia una clave con un valor. Para obtener un dato concreto necesitamos conocer su clave: $ticket['asunto'].
También podemos recorrer clave y valor:
<?php
foreach ($ticket as $clave => $valor) {
echo $clave . ': ' . $valor . '<br>';
}
7.6. Una lista de tickets: array de arrays
<?php
$tickets = [
['codigo' => 'IAW-07', 'estado' => 'abierto'],
['codigo' => 'IAW-08', 'estado' => 'cerrado'],
['codigo' => 'IAW-09', 'estado' => 'abierto'],
];
$abiertos = 0;
foreach ($tickets as $ticket) {
if ($ticket['estado'] === 'abierto') {
$abiertos++;
}
}
echo 'Tickets abiertos: ' . $abiertos; // 2
Este ejemplo integra piezas ya aprendidas: el array reúne datos; foreach recorre; if decide; $abiertos++ actualiza el contador; echo muestra el resultado.
7.7. Comprobar claves y trabajar con valores opcionales
Si consultamos una clave inexistente como $ticket['responsable'], PHP puede avisar de que la clave no está definida. Cuando un dato puede faltar, utilizaremos una comprobación:
<?php
$ticket = ['asunto' => 'Error de acceso'];
$responsable = $ticket['responsable'] ?? 'Sin asignar';
echo $responsable;
El operador ?? devuelve el valor de la izquierda si existe y no es null; en caso contrario, utiliza el valor alternativo. isset($ticket['responsable']) permite comprobar si la clave está definida y no es nula.
Ejercicios 7.1–7.11. Define cinco módulos; muestra el primero y el último; modifica un elemento; añade otro; recorre un array con for; recórrelo con foreach; calcula la media de notas de un array no vacío; encuentra el máximo mediante una variable auxiliar; crea un ticket asociativo; recorre un ticket mostrando claves y valores; cuenta cuántos tickets están abiertos en una lista. Ampliación: explica por qué acceder mediante $i a un array asociativo no equivale a recorrerlo con foreach.
8. Entrada de datos en PHP: terminal, formularios y peticiones HTTP

Hasta ahora muchos valores estaban escritos en el propio código. Una aplicación real necesita recibir datos desde fuera. En PHP pueden llegar por terminal, URL, formulario, fichero subido, cookie, sesión o cuerpo JSON. La secuencia de trabajo será siempre explícita:
RECIBIR → COMPROBAR TIPO → NORMALIZAR → VALIDAR → USAR → ESCAPAR AL MOSTRAR
8.1. Entrada por terminal: readline(), STDIN y $argv
En CLI PHP sí puede esperar a que escribamos:
<?php
$nombre = readline('Escribe tu nombre: ');
$nombre = trim($nombre);
echo 'Hola, ' . $nombre . PHP_EOL;
También:
$linea = fgets(STDIN);
o argumentos de arranque:
php script.php Ana 7
$nombre = $argv[1] ?? 'Sin nombre';
$puesto = $argv[2] ?? 'NN';
Esto sirve para scripts y automatización. No es la forma habitual de recoger el teclado de un usuario web.
8.2. En una aplicación web PHP no se queda esperando al usuario
GET /contacto.php
↓
PHP genera HTML y termina
↓
usuario escribe en el navegador
↓
POST /contacto.php
↓
PHP vuelve a ejecutarse
↓
$_POST
Aunque utilicemos el mismo fichero PHP, son peticiones y ejecuciones diferentes.
8.3. Formulario y procesamiento en archivos distintos
formulario.php:
<form action="procesar.php" method="post">
<label for="nombre">Nombre</label>
<input id="nombre" name="nombre">
<button>Enviar</button>
</form>
procesar.php:
<?php
$valor = $_POST['nombre'] ?? '';
if (!is_string($valor)) {
http_response_code(422);
exit('Formato incorrecto');
}
$nombre = trim($valor);
echo htmlspecialchars(
$nombre,
ENT_QUOTES | ENT_SUBSTITUTE,
'UTF-8'
);
Es la opción más clara para comprender el recorrido inicial.
8.4. Formulario y procesamiento en el mismo fichero
También podemos utilizar una sola URL:
<?php
$nombre = '';
$mensaje = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$valor = $_POST['nombre'] ?? '';
if (is_string($valor)) {
$nombre = trim($valor);
if ($nombre !== '') {
$mensaje = 'Hola, ' . $nombre;
}
}
}
function e(string $texto): string
{
return htmlspecialchars(
$texto,
ENT_QUOTES | ENT_SUBSTITUTE,
'UTF-8'
);
}
?>
<!doctype html>
<html lang="es">
<body>
<?php if ($mensaje !== ''): ?>
<p><?= e($mensaje) ?></p>
<?php endif; ?>
<form method="post">
<input name="nombre" value="<?= e($nombre) ?>">
<button>Enviar</button>
</form>
</body>
</html>
Al omitir action, el formulario se envía a la URL actual.
| Diseño | Ventaja |
|---|---|
| Formulario + procesador separados | Flujo muy visible |
| Mismo fichero | Adecuado para formularios pequeños |
| Handler/controlador separado | Mejor separación en aplicaciones mayores |
| POST → 303 → GET | Evita reenvíos al recargar |
8.5. id, name, action y method
id: identifica el control en HTML y lo relaciona conlabel.name: define la clave enviada a PHP.action: indica qué recurso recibirá el formulario.method: indica GET o POST.
<input id="asunto" name="asunto">
se recupera como:
$_POST['asunto']
si el formulario usa POST.
8.6. GET y POST
Con GET:
/buscar.php?q=apache
$q = $_GET['q'] ?? '';
Con POST:
$asunto = $_POST['asunto'] ?? '';
| GET | POST |
|---|---|
| Parámetros en URL | Datos normalmente en el cuerpo |
| Búsquedas, filtros, navegación | Altas, ediciones, acciones |
| URL compartible | El cuerpo no forma parte de la URL visible |
$_GET |
$_POST |
POST no cifra los datos. HTTPS protege el transporte.
8.7. Superglobales que debes reconocer
| Superglobal | Contenido |
|---|---|
$_GET |
Parámetros de URL |
$_POST |
Campos POST compatibles |
$_FILES |
Ficheros subidos |
$_COOKIE |
Cookies |
$_SERVER |
Información de petición/servidor |
$_SESSION |
Estado de sesión tras session_start() |
$_ENV |
Variables de entorno disponibles |
Evitaremos $_REQUEST como atajo porque oculta de qué fuente procede un dato.
8.8. El navegador no convierte una entrada externa en dato fiable
<input type="number" name="edad">
no evita que llegue una petición manipulada. En servidor:
$raw = $_POST['edad'] ?? '';
if (!is_string($raw)) {
exit('Formato incorrecto');
}
$edad = filter_var(
trim($raw),
FILTER_VALIDATE_INT
);
if ($edad === false || $edad < 0 || $edad > 130) {
exit('Edad no válida');
}
La validación HTML mejora la experiencia, pero no sustituye la validación PHP.
8.9. El dato puede llegar con una forma distinta de la esperada
Esperamos:
?nombre=Ana
pero pueden enviar:
?nombre[]=Ana
Por eso:
$raw = $_GET['nombre'] ?? '';
if (!is_string($raw)) {
http_response_code(422);
exit('Se esperaba texto');
}
es más robusto que llamar directamente trim($_GET['nombre']).
8.10. Checkbox, radio, select y datos múltiples
Un checkbox no marcado normalmente no envía su clave:
$acepta = ($_POST['acepta'] ?? '') === 'si';
Un valor seleccionado debe validarse:
$prioridad = $_POST['prioridad'] ?? '';
if (
!is_string($prioridad)
|| !in_array($prioridad, ['baja', 'media', 'alta'], true)
) {
exit('Prioridad no válida');
}
Para varios valores:
<input type="checkbox" name="etiquetas[]" value="apache">
<input type="checkbox" name="etiquetas[]" value="php">
esperamos:
$etiquetas = $_POST['etiquetas'] ?? [];
if (!is_array($etiquetas)) {
$etiquetas = [];
}
8.11. Normalizar, validar, escapar y almacenar
Son responsabilidades diferentes:
NORMALIZAR → trim(), formato coherente
VALIDAR → aceptar/rechazar
ESCAPAR → preparar para HTML u otro contexto
ALMACENAR → fichero, sesión, MariaDB...
Ejemplo de salida HTML:
echo htmlspecialchars(
$asunto,
ENT_QUOTES | ENT_SUBSTITUTE,
'UTF-8'
);
htmlspecialchars() no valida un email ni una prioridad.
8.12. Método HTTP y códigos de respuesta
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit('Este recurso espera POST');
}
Códigos que iremos utilizando:
200: correcto.303: redirección después de POST.400: petición mal formada.401: falta autenticación.403: sin autorización.404: recurso no encontrado.405: método no permitido.422: datos no aceptables.500: error interno.
8.13. POST/Redirect/GET
Después de una operación correcta:
header('Location: confirmacion.php', true, 303);
exit;
POST → procesar → 303 → GET → página final
Evita que F5 repita accidentalmente el POST.
8.14. Ficheros subidos: $_FILES
Con:
<form method="post" enctype="multipart/form-data">
<input type="file" name="adjunto">
</form>
PHP recibe metadatos en:
$_FILES['adjunto']
En el proyecto comprobaremos error de subida, tamaño, MIME real con finfo, nombre generado por la aplicación, move_uploaded_file() y almacenamiento fuera de public/.
8.15. JSON en el cuerpo de una petición
Un cliente puede enviar:
Content-Type: application/json
Ese contenido no aparece automáticamente en $_POST.
$cuerpo = file_get_contents('php://input');
$datos = json_decode(
$cuerpo,
true,
512,
JSON_THROW_ON_ERROR
);
Es una ampliación profesional; primero dominaremos formularios.
8.16. Comprobación de aprendizaje
Debes poder explicar:
- Por qué
readline()y un formulario web son escenarios distintos. - Relación entre
name="asunto"y$_POST['asunto']. - Diferencia GET/POST.
- Por qué mismo fichero no significa misma ejecución.
- Por qué se comprueba el tipo antes de
trim(). - Qué ocurre con un checkbox no marcado.
- Cuándo esperamos un array.
- Diferencia entre validar y escapar.
- Por qué
type="number"no basta. - Para qué sirve PRG.
9. Funciones incorporadas (built-in functions)
PHP proporciona miles de funciones. No hay que memorizarlas todas: hay que aprender a buscar, leer parámetros, interpretar el retorno y comprobar casos límite.
9.1. Función, construcción del lenguaje y método
trim($texto); // función incorporada
normalizar($texto); // función propia
$ticket->cerrar(); // método de instancia
Ticket::crear(); // método estático
Mientras que:
echo, include, require, isset, empty, exit
son construcciones del lenguaje.
9.2. Anatomía de una llamada
$longitud = strlen($texto);
strlen: nombre.$texto: argumento.$longitud: recibe el retorno.
Siempre preguntaremos: qué recibe, qué devuelve, cómo indica fallo y si modifica algo.
9.3. Cadenas
trim()
strlen()
mb_strlen()
strtolower() / strtoupper()
mb_strtolower() / mb_strtoupper()
str_contains()
str_starts_with()
str_ends_with()
substr()
str_replace()
explode()
implode()
Ejemplo:
$entrada = ' PHP,Apache,MariaDB ';
$partes = explode(',', trim($entrada));
echo implode(' | ', $partes);
strlen() cuenta bytes. Para texto UTF-8 humano, mb_strlen() suele expresar mejor el número de caracteres esperado.
9.4. Arrays
count()
in_array()
array_key_exists()
array_keys()
array_values()
array_merge()
array_map()
array_filter()
array_sum()
sort() / rsort()
usort()
Cuando proceda:
in_array($prioridad, $permitidas, true);
utiliza comparación estricta.
9.5. Tipos y validación
is_string()
is_int()
is_float()
is_bool()
is_array()
is_object()
is_null()
is_numeric()
filter_var()
isset() es una construcción del lenguaje. array_key_exists() comprueba una clave incluso cuando su valor es null.
9.6. Números, fechas y formato
round()
min()
max()
abs()
pow()
sqrt()
number_format()
time()
date()
También veremos objetos:
$fecha = new DateTimeImmutable(
'now',
new DateTimeZone('Europe/Madrid')
);
echo $fecha->format(DateTimeInterface::ATOM);
Aquí format() es un método.
9.7. Web, seguridad, JSON y ficheros
Funciones especialmente relevantes:
htmlspecialchars()
header()
http_response_code()
setcookie()
session_start()
session_regenerate_id()
password_hash()
password_verify()
random_bytes()
json_encode()
json_decode()
file_get_contents()
file_put_contents()
fopen()
fwrite()
fclose()
9.8. Depuración
var_dump($dato);
print_r($array);
Son útiles en desarrollo. No debemos publicar volcados con secretos o datos sensibles.
9.9. Cómo estudiar una built-in
Ejemplo:
NOMBRE: trim
RECIBE: string
DEVUELVE: string
MODIFICA ORIGINAL: no
FALLO: no aplicable en uso normal
CASO LÍMITE: cadena vacía
Ese método de análisis es más importante que memorizar listas.
9.10. Built-ins que reutilizaremos en el Proyecto 1
A partir de ahora no estudiaremos las funciones incorporadas como un catálogo. Las reutilizaremos cuando aparezca una necesidad concreta del proyecto.
| Necesidad | Funciones / recursos que aparecerán |
|---|---|
| Normalizar y comprobar texto | trim(), mb_strlen(), str_contains() |
| Comprobar valores admitidos | in_array(..., true), array_key_exists() |
| Validar email o entero | filter_var() |
| Escapar salida HTML | htmlspecialchars() |
| Depurar durante desarrollo | var_dump(), print_r(), get_debug_type() |
| Calcular páginas | ceil(), max(), min() |
| Contraseñas | password_hash(), password_verify() |
| Tokens aleatorios | random_bytes(), bin2hex() |
| Comparar tokens CSRF | hash_equals() |
| Sesiones | session_start(), session_regenerate_id() |
| Redirección PRG | header() |
| Tipo real de un fichero | finfo / finfo_file() |
| Subida HTTP | move_uploaded_file() |
| Comprobar almacenamiento | file_exists(), is_file(), is_readable() |
| JSON | json_encode(), json_decode() |
| Registro técnico | error_log() |
Ejemplo 1 · Validar una prioridad
$permitidas = ['baja', 'media', 'alta'];
if (!in_array($prioridad, $permitidas, true)) {
exit('Prioridad no permitida');
}
El tercer argumento true exige comparación estricta. En validaciones no queremos que una conversión automática de tipos haga coincidir valores inesperados.
Ejemplo 2 · Validar un email
$email = trim($email);
if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
$errores[] = 'Email no válido';
}
Validar y escapar siguen siendo operaciones distintas. filter_var() decide si el dato cumple una regla; htmlspecialchars() se utiliza al incluir texto en HTML.
Ejemplo 3 · Número total de páginas
$totalRegistros = 73;
$porPagina = 10;
$totalPaginas = (int) ceil(
$totalRegistros / $porPagina
);
En UT03, $totalRegistros procederá de una consulta COUNT(*) y cada página se obtendrá con LIMIT y OFFSET. No cargaremos todos los registros para ocultarlos después con JavaScript.
Ejemplo 4 · Contraseñas
$hash = password_hash(
$password,
PASSWORD_DEFAULT
);
if (password_verify($passwordRecibida, $hash)) {
// Credencial correcta.
}
La aplicación almacena el hash, no la contraseña original. El Proyecto 1 utilizará esta pareja de funciones durante el registro/creación de usuarios y el login.
Ejemplo 5 · Token aleatorio y comparación segura
$token = bin2hex(
random_bytes(32)
);
if (!hash_equals($tokenGuardado, $tokenRecibido)) {
http_response_code(403);
exit('Token incorrecto');
}
Este patrón aparecerá de nuevo cuando implementemos protección CSRF con un token asociado a la sesión.
Ejemplo 6 · Nombre de fichero generado por la aplicación
$nombreSeguro = bin2hex(
random_bytes(16)
) . '.pdf';
El nombre original proporcionado por el navegador no debe utilizarse como nombre final del fichero almacenado. Antes habrá que validar tamaño y MIME real.
Ejemplo 7 · Comprobar el MIME real de una subida
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($rutaTemporal);
$permitidos = [
'image/jpeg',
'image/png',
'application/pdf',
];
if (!is_string($mime)
|| !in_array($mime, $permitidos, true)) {
exit('Tipo de fichero no permitido');
}
La extensión .jpg escrita en el nombre no demuestra que el contenido sea realmente una imagen JPEG.
Ejemplo 8 · Registrar un fallo sin mostrarlo al usuario
error_log(
'IAW Desk: error al guardar un ticket'
);
El mensaje que ve el usuario y el detalle técnico de diagnóstico no tienen por qué ser el mismo. Nunca escribiremos contraseñas, tokens de sesión ni credenciales SQL en un log.
Regla de estudio: no memorices veinte firmas. Localiza la necesidad, busca la función adecuada en el manual, revisa sus parámetros y su valor de retorno, prueba un caso normal y uno límite y después intégrala en tu código.
Ejercicios 9.1–9.10. Limpia y mide una cadena; divide una lista separada por comas y vuelve a unirla; diferencia isset() de array_key_exists() cuando el valor es null; valida un email; calcula las páginas necesarias para 73 registros a 10 por página; genera y verifica un hash de contraseña; genera un token aleatorio hexadecimal; compara dos tokens con hash_equals(); identifica el MIME real de un fichero de prueba; registra mediante error_log() una incidencia que no contenga información sensible.
10. Funciones propias y procedimientos

Una función da nombre a una operación reutilizable.
10.1. Definir y llamar
function mostrarSaludo(): void
{
echo 'Bienvenido a IAW';
}
mostrarSaludo();
Definir no es ejecutar.
10.2. Parámetro y argumento
function saludar(string $nombre): string
{
return 'Hola, ' . $nombre;
}
saludar('Ana');
$nombre es parámetro; 'Ana' es argumento.
10.3. return, echo y «procedimiento»
function sumar(int $a, int $b): int
{
return $a + $b;
}
return entrega el resultado al llamador.
function mostrarMensaje(string $texto): void
{
echo $texto;
}
En terminología didáctica tradicional podemos llamar procedimiento a una función orientada a realizar una acción y sin valor de retorno útil. PHP no tiene una palabra procedure: sigue siendo una función, normalmente con retorno void.
10.4. Responsabilidad única
Preferimos:
normalizarTexto()
validarTicket()
prioridadValida()
calcularImporte()
a una función procesarTodo() que lea POST, valide, consulte BBDD, genere HTML y escriba logs al mismo tiempo.
10.5. Tipos y strict_types
declare(strict_types=1);
function calcularCoste(
float $horas,
float $tarifa
): float {
return $horas * $tarifa;
}
El tipado ayuda a expresar contratos. No sustituye la validación de la entrada HTTP.
10.6. Parámetros opcionales
function saludar(
string $nombre,
string $prefijo = 'Hola'
): string {
return $prefijo . ', ' . $nombre;
}
10.7. Argumentos con nombre
crearCodigo(
modulo: 'IAW',
puesto: 7,
prefijo: 'TICKET'
);
Mejoran legibilidad en ciertos casos, aunque convierten los nombres de parámetros en parte práctica de la API.
10.8. Variádicos y desempaquetado
function sumarTodo(int ...$numeros): int
{
return array_sum($numeros);
}
echo sumarTodo(1, 2, 3);
Y:
$valores = [4, 5, 6];
echo sumarTodo(...$valores);
10.9. Paso por valor
function duplicar(int $numero): int
{
return $numero * 2;
}
$original = 5;
$doble = duplicar($original);
$original sigue valiendo 5.
10.10. Paso por referencia
function incrementar(int &$numero): void
{
$numero++;
}
El & permite modificar deliberadamente la variable del llamador. Para cálculos suele ser más claro return.
10.11. Arrays de entrada y salida
function validarTicket(
string $asunto,
string $prioridad
): array {
$errores = [];
if (trim($asunto) === '') {
$errores['asunto'] = 'Obligatorio';
}
if (
!in_array(
$prioridad,
['baja', 'media', 'alta'],
true
)
) {
$errores['prioridad'] = 'No válida';
}
return $errores;
}
Las funciones booleanas son especialmente expresivas:
function prioridadValida(string $prioridad): bool
{
return in_array(
$prioridad,
['baja', 'media', 'alta'],
true
);
}
10.12. Ámbito
Las variables declaradas dentro de una función tienen ámbito local.
Evitaremos global como mecanismo habitual; preferiremos parámetros y retornos.
10.13. Callbacks y callable
function transformar(
array $valores,
callable $operacion
): array {
return array_map($operacion, $valores);
}
Una función entregada a otra para que la ejecute es un callback.
Aparece en:
array_map()
array_filter()
usort()
10.14. Funciones anónimas y closures
$doble = function (int $n): int {
return $n * 2;
};
Captura de variables externas:
$iva = 0.21;
$calcular = function (float $base) use ($iva): float {
return $base * (1 + $iva);
};
10.15. Arrow functions
$doble = fn(int $n): int => $n * 2;
Son adecuadas para expresiones breves:
$dobles = array_map(
fn(int $n): int => $n * 2,
[1, 2, 3]
);
10.16. Recursividad
function factorial(int $n): int
{
if ($n <= 1) {
return 1;
}
return $n * factorial($n - 1);
}
Debe existir un caso base. La mayoría del proyecto no necesitará recursividad, pero conviene reconocerla.
10.17. Efectos secundarios y diseño
Una función puede modificar sesión, fichero, BBDD o argumentos por referencia. Cuando sea razonable preferiremos operaciones predecibles que reciban datos y devuelvan resultados.
Una buena función intenta tener:
nombre claro
una responsabilidad
parámetros comprensibles
retorno conocido
pocos efectos secundarios
casos límite comprobables
10.18. Funciones que aparecerán en el proyecto
normalizarTexto()
validarTicket()
prioridadValida()
puedeEditar()
generarNombreSeguro()
obtenerConexion()
buscarTickets()
crearTicket()
registrarAuditoria()
generarCsrfToken()
validarCsrf()
No las implementaremos todas ahora, pero debemos entender el patrón.
11. Modularidad: repartir responsabilidades

Una aplicación no debe crecer dentro de un único index.php.
/srv/iaw/php/
├── public/
│ ├── index.php
│ └── procesar.php
├── src/
│ └── funciones.php
├── templates/
│ └── resultado.php
└── var/
├── data/
├── logs/
└── uploads/
11.1. include, require y variantes _once
| Recurso | Idea |
|---|---|
include |
Incluye; el programa puede continuar tras ciertos fallos |
require |
El archivo es imprescindible |
include_once |
Incluye una vez |
require_once |
Requiere una vez |
require_once
__DIR__
. '/../src/funciones.php';
11.2. __DIR__
Contiene el directorio del archivo actual y ayuda a construir rutas fiables.
11.3. Entrada, lógica y presentación
public/procesar.php → recibe HTTP
src/funciones.php → lógica reutilizable
templates/*.php → HTML
No buscamos muchos archivos: buscamos responsabilidades localizables.
12. Métodos y programación orientada a objetos
Una función puede existir de forma independiente. Un método se declara dentro de una clase.
trim($texto); // función incorporada
normalizar($texto); // función propia
$ticket->cerrar(); // método de instancia
Validador::email(); // método estático

12.1. Clase, objeto, propiedad y método
class Ticket
{
public string $asunto = '';
public function resumen(): string
{
return 'Ticket: ' . $this->asunto;
}
}
$ticket = new Ticket();
$ticket->asunto = 'Apache no responde';
echo $ticket->resumen();
12.2. $this
Dentro de un método de instancia, $this representa el objeto concreto que ejecuta el método.
12.3. Constructor
class Ticket
{
public function __construct(
public string $asunto,
public string $estado = 'abierto'
) {
}
}
La promoción de propiedades anterior es compacta; antes conviene comprender la versión explícita:
public string $asunto;
public function __construct(string $asunto)
{
$this->asunto = $asunto;
}
12.4. Visibilidad y encapsulación
public → accesible desde fuera
private → solo desde la clase
protected → clase y descendientes
private string $estado = 'abierto';
public function cerrar(): void
{
$this->estado = 'cerrado';
}
La encapsulación protege reglas del objeto.
12.5. Métodos que devuelven y métodos void
public function resumen(): string
{
return $this->asunto;
}
frente a:
public function cerrar(): void
{
$this->estado = 'cerrado';
}
12.6. Métodos estáticos y ::
class Validador
{
public static function emailValido(string $email): bool
{
return filter_var(
$email,
FILTER_VALIDATE_EMAIL
) !== false;
}
}
Validador::emailValido('ana@example.com');
Un método static no dispone de $this.
12.7. self::, parent::, herencia y sobrescritura
class Ticket
{
public function etiqueta(): string
{
return 'Ticket';
}
}
class IncidenciaCritica extends Ticket
{
public function etiqueta(): string
{
return parent::etiqueta() . ' CRÍTICO';
}
}
La clase hija sobrescribe el método. No construiremos jerarquías profundas sin necesidad.
12.8. Interfaces
interface Exportable
{
public function exportar(): string;
}
Una interfaz expresa un contrato que una clase puede implementar.
12.9. Traits
trait MarcaTemporal
{
public function ahoraIso(): string
{
return date(DATE_ATOM);
}
}
Un trait permite reutilizar miembros entre clases sin crear una relación de herencia.
12.10. Métodos mágicos
Conviene reconocer:
__construct()
__destruct()
__toString()
__get()
__set()
__call()
Dominaremos __construct(). El resto se estudiará cuando exista una necesidad real.
12.11. PHP no sobrecarga métodos por firma como Java
No declararemos dos métodos con el mismo nombre diferenciados únicamente por sus parámetros. PHP resuelve necesidades similares con parámetros opcionales, variádicos u otros mecanismos.
12.12. ¿Función o método?
Función:
recibe datos → calcula/valida → devuelve
Método:
la operación pertenece a un objeto concreto
Ejemplos:
prioridadValida() → función
normalizarTexto() → función
$ticket->cerrar() → método
$usuario->cambiarClave() → método
No todo necesita una clase.
12.13. Comprobación de aprendizaje
Debes poder explicar:
- Función incorporada vs propia.
- Procedimiento como función
void. - Parámetro vs argumento.
echovsreturn.- Paso por valor vs referencia.
- Parámetro opcional, variádico y argumento con nombre.
- Callback, función anónima y arrow function.
- Función vs método.
$this.- Constructor y visibilidad.
- Método de instancia vs estático.
- Herencia, interfaz y trait a nivel de reconocimiento.
- Por qué no todo debe convertirse en una clase.
Ejercicios integradores. Convierte validaciones repetidas en funciones; crea funciones que reciban y devuelvan arrays; prueba valor/referencia; utiliza array_filter() con callback; reescribe un callback simple mediante fn; separa validadores en src/funciones.php; crea una clase Ticket con constructor, cerrar() y estaCerrado(); compara prioridadValida() con $ticket->cerrar() y justifica su ubicación.
13. Persistencia ligera, logs y ficheros seguros
Hasta ahora, las variables existen mientras se ejecuta una petición. Cuando termina el script, una variable normal desaparece. Antes de introducir MariaDB conviene conocer una forma de persistencia sencilla: guardar datos en el sistema de ficheros del servidor.
Un fichero puede ser útil para un registro de actividad, una configuración sencilla, una exportación o una pequeña colección de datos. No sustituye a una base de datos cuando necesitamos consultas complejas, relaciones, muchos accesos concurrentes o transacciones. Precisamente por eso resulta útil estudiarlo primero: nos permite distinguir memoria, fichero, sesión y base de datos.
| Mecanismo | ¿Sobrevive a otra petición? | ¿Dónde vive principalmente? | Uso didáctico |
|---|---|---|---|
| Variable PHP normal | No | Memoria del proceso durante la petición | Cálculos temporales |
| Fichero | Sí, mientras el almacenamiento permanezca | Disco del servidor | Logs, JSON, CSV, datos sencillos |
| Sesión | Sí, entre peticiones de una sesión | Servidor + identificador en cliente | Estado de navegación e identidad |
| Base de datos | Sí | SGBD | Datos estructurados y concurrentes |
13.1. Dónde guardar datos que no deben ser públicos
No escribiremos logs ni datos internos dentro del DocumentRoot. Amplía la estructura del proyecto:
/srv/iaw/php/
├── public/
│ └── index.php ← accesible por HTTP
├── src/
├── templates/
└── var/ ← NO accesible directamente por HTTP
├── data/
├── logs/
└── uploads/
En el laboratorio, el administrador puede preparar las carpetas con permisos de escritura para el usuario de desarrollo y para el grupo utilizado por el servicio web, sin recurrir a chmod 777:
sudo install -d -m 2770 -o "$USER" -g www-data /srv/iaw/php/var
sudo install -d -m 2770 -o "$USER" -g www-data /srv/iaw/php/var/data
sudo install -d -m 2770 -o "$USER" -g www-data /srv/iaw/php/var/logs
sudo install -d -m 2770 -o "$USER" -g www-data /srv/iaw/php/var/uploads
Después comprueba las rutas y permisos con namei -l y ls -ld. Si PHP-FPM no puede escribir, no abras los permisos a todo el sistema: identifica qué usuario/grupo ejecuta el proceso y corrige la propiedad o los permisos necesarios.
13.2. Primer fichero: escribir una línea
file_put_contents() permite escribir una cadena completa en un fichero. Empezaremos con un registro muy pequeño:
<?php
$ruta = __DIR__ . '/../var/logs/app.log';
$linea = date('c') . " inicio_aplicacion\n";
$resultado = file_put_contents($ruta, $linea);
if ($resultado === false) {
echo 'No se pudo escribir el log';
}
date('c') genera una fecha y hora en formato ISO 8601. __DIR__ representa la carpeta del archivo PHP actual; al partir de un script situado en public/, ../var/logs/app.log apunta fuera de la carpeta publicada.
Atención: sin opciones adicionales, file_put_contents() reemplaza el contenido anterior del fichero. Para un log normalmente queremos añadir, no borrar.
13.3. Añadir al final: un log acumulativo
<?php
$ruta = __DIR__ . '/../var/logs/app.log';
$codigo = 'IAW-07';
$linea = date('c') . " ticket=$codigo accion=consulta\n";
$resultado = file_put_contents(
$ruta,
$linea,
FILE_APPEND | LOCK_EX
);
if ($resultado === false) {
http_response_code(500);
echo 'No se pudo registrar la operación';
}
FILE_APPENDañade la cadena al final en lugar de truncar el fichero.LOCK_EXsolicita un bloqueo exclusivo durante la escritura para reducir problemas cuando dos escrituras coinciden.- El valor devuelto es el número de bytes escritos o
falsesi la operación falla.
Un bloqueo de fichero no convierte un archivo de texto en una base de datos transaccional. Sirve para un caso pequeño y controlado; después compararemos sus límites con MariaDB.
13.4. Leer un fichero de texto
file_get_contents() devuelve el contenido completo como cadena o false si no puede leerlo:
<?php
$ruta = __DIR__ . '/../var/logs/app.log';
$contenido = file_get_contents($ruta);
if ($contenido === false) {
http_response_code(500);
exit('No se pudo leer el log');
}
echo '<pre>' . htmlspecialchars(
$contenido,
ENT_QUOTES | ENT_SUBSTITUTE,
'UTF-8'
) . '</pre>';
Escapamos el texto antes de mostrarlo en HTML. Un log puede contener caracteres enviados por usuarios o por otros sistemas; no debemos convertir accidentalmente esas cadenas en marcado ejecutable.
13.5. Datos estructurados sencillos con JSON
Un log es una sucesión de líneas. Para conservar una pequeña estructura de datos podemos serializar un array como JSON. Supongamos que todavía no existe MariaDB y queremos guardar dos tickets de prueba:
<?php
$tickets = [
[
'codigo' => 'IAW-07',
'asunto' => 'Apache no responde',
'estado' => 'abierto',
],
[
'codigo' => 'IAW-08',
'asunto' => 'Error de acceso',
'estado' => 'cerrado',
],
];
$json = json_encode(
$tickets,
JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE
);
if ($json === false) {
exit('No se pudieron convertir los datos a JSON');
}
$ruta = __DIR__ . '/../var/data/tickets.json';
if (file_put_contents($ruta, $json, LOCK_EX) === false) {
exit('No se pudieron guardar los datos');
}
json_encode() transforma el array PHP en texto JSON. Guardar ese texto permite recuperarlo en otra petición. Para leerlo:
<?php
$ruta = __DIR__ . '/../var/data/tickets.json';
$contenido = file_get_contents($ruta);
if ($contenido === false) {
exit('No se pudo leer el fichero de datos');
}
$tickets = json_decode($contenido, true);
if (!is_array($tickets)) {
exit('El JSON no contiene la estructura esperada');
}
foreach ($tickets as $ticket) {
echo htmlspecialchars(
(string) ($ticket['codigo'] ?? 'sin-codigo'),
ENT_QUOTES | ENT_SUBSTITUTE,
'UTF-8'
) . '<br>';
}
El segundo argumento true de json_decode() pide arrays asociativos. Aquí reutilizamos arrays y foreach: la persistencia no introduce otra forma de programar, sino otra fuente y destino de los datos.
13.6. fopen, modos de apertura y escritura por flujo
Las funciones anteriores resuelven muchos casos sencillos. Cuando necesitamos más control podemos abrir explícitamente un recurso con fopen(). Los modos básicos que conviene reconocer son:
| Modo | Significado | Riesgo principal |
|---|---|---|
r |
Solo lectura; el fichero debe existir | Falla si no existe |
w |
Escritura desde el principio | Trunca el contenido anterior |
a |
Escritura al final | Siempre añade |
Ejemplo equivalente de log con apertura en modo de anexado:
<?php
$ruta = __DIR__ . '/../var/logs/app.log';
$recurso = fopen($ruta, 'a');
if ($recurso === false) {
exit('No se pudo abrir el log');
}
fwrite($recurso, date('c') . " accion=prueba\n");
fclose($recurso);
Abrir, utilizar y cerrar explícitamente ayuda a comprender el ciclo de trabajo con un fichero. Para operaciones pequeñas preferiremos la solución más simple que permita controlar correctamente los errores.
13.7. Log de aplicación y log de errores no son lo mismo
Un log de aplicación puede registrar hechos previstos: creación de un ticket, cambio de estado o una importación. Un log de errores registra problemas técnicos que interesa diagnosticar. PHP ofrece error_log() para enviar un mensaje al mecanismo de registro configurado por el entorno:
<?php
$ok = false;
if (!$ok) {
error_log('IAW Desk: fallo controlado durante una prueba');
}
En nuestro laboratorio, según la configuración de Apache y PHP-FPM, esos mensajes podrán aparecer en los logs del sitio o del servicio. No debemos mostrar al usuario una traza interna completa cuando la aplicación está en un entorno de producción.
Nunca registres contraseñas, cookies de sesión, claves API ni secretos de base de datos. Un log también es un dato que debe protegerse, limitarse y rotarse cuando crece.
13.8. ¿Cuándo deja de ser buena idea un fichero?
Los ficheros son apropiados para aprender persistencia ligera y para algunos usos reales, especialmente logs, exportaciones y configuraciones. Empiezan a resultar incómodos cuando necesitamos:
- buscar y filtrar muchos registros;
- relacionar usuarios, tickets y estados;
- evitar actualizaciones concurrentes conflictivas;
- imponer restricciones como claves únicas;
- ejecutar varias operaciones como una unidad atómica;
- mantener índices, consultas y permisos de datos de forma estructurada.
Ese problema prepara la siguiente unidad: UT03 introduce PDO y MariaDB. No sustituimos un fichero porque sea «malo»; elegimos otro mecanismo porque cambian los requisitos.
Ejercicios 13.1–13.8. Escribe una línea con fecha en app.log; añade cinco operaciones sin borrar las anteriores; muestra el log escapado en una página; crea un contador persistente en un fichero; guarda un array de tickets en JSON; léelo y genera una lista HTML; provoca un JSON incorrecto y controla el fallo; explica qué datos guardarías en variable, fichero, sesión o base de datos y justifica cada decisión.
13.9. Subida de ficheros: $_FILES no basta por sí solo
Un formulario de subida necesita enctype="multipart/form-data":
<form method="post" enctype="multipart/form-data">
<input type="file" name="adjunto">
<button>Subir</button>
</form>
PHP expone la información recibida en $_FILES. Para un campo llamado adjunto aparecen datos como name, tmp_name, error y size. El nombre que envía el navegador es un dato aportado por el cliente, no un nombre de almacenamiento fiable.
La secuencia segura que utilizaremos en el Proyecto 1 será:
$_FILES
↓
comprobar error
↓
comprobar tamaño
↓
detectar MIME real
↓
comprobar tipo permitido
↓
generar nombre interno
↓
move_uploaded_file()
↓
guardar metadatos
Ejemplo reducido:
<?php
$fichero = $_FILES['adjunto'] ?? null;
if (!is_array($fichero)) {
exit('No llegó el fichero');
}
if (($fichero['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
exit('Error durante la subida');
}
if ((int) $fichero['size'] > 2 * 1024 * 1024) {
exit('Fichero demasiado grande');
}
$tmp = $fichero['tmp_name'] ?? '';
if (!is_string($tmp) || !is_uploaded_file($tmp)) {
exit('La subida temporal no es válida');
}
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($tmp);
$permitidos = [
'application/pdf' => 'pdf',
'image/jpeg' => 'jpg',
'image/png' => 'png',
];
if (!is_string($mime) || !isset($permitidos[$mime])) {
exit('Tipo no permitido');
}
$nombreInterno =
bin2hex(random_bytes(16))
. '.'
. $permitidos[$mime];
$directorio =
__DIR__
. '/../var/uploads';
if (!is_dir($directorio) || !is_writable($directorio)) {
error_log('IAW Desk: directorio de subidas no disponible');
exit('No se puede almacenar el fichero');
}
$destino =
$directorio
. '/'
. $nombreInterno;
if (!move_uploaded_file($tmp, $destino)) {
error_log('IAW Desk: move_uploaded_file() ha fallado');
exit('No se pudo almacenar el fichero');
}
No confundas tres nombres: el nombre original puede conservarse como metadato para mostrarlo al usuario; el fichero temporal lo crea PHP durante la subida; el nombre físico definitivo debe decidirlo la aplicación.
13.10. Descarga controlada: el cliente no debe escoger una ruta del servidor
No construiremos una descarga aceptando directamente:
download.php?file=../../algo
La aplicación recibirá un identificador lógico y decidirá qué fichero corresponde a él después de comprobar identidad y permisos.
GET /download.php?id=17
↓
validar id
↓
buscar metadatos
↓
comprobar autorización
↓
resolver ruta controlada
↓
enviar fichero
Cuando incorporemos MariaDB, los metadatos del adjunto —registro al que pertenece, nombre interno, nombre original, MIME, tamaño y propietario— podrán persistirse en la base de datos mientras los bytes permanecen en un almacenamiento controlado.
13.11. Qué papel tendrán los ficheros en el Proyecto 1
El requisito del proyecto no consiste únicamente en «subir un archivo». El alumno deberá poder explicar y demostrar:
- que existe un tamaño máximo;
- que se comprueba el MIME real;
- que el nombre físico no es el proporcionado por el navegador;
- que el fichero queda vinculado a un registro;
- que una descarga privada comprueba autorización;
- que los errores no exponen rutas internas innecesarias.
14. Estado entre peticiones: cookies, sesiones y autenticación

14.1. El problema: una petición PHP termina
Una variable PHP ordinaria vive durante una ejecución. Si cada petición ejecuta:
<?php
$contador = 0;
$contador++;
echo $contador;
veremos 1 en cada recarga. La nueva petición vuelve a empezar desde el principio.
PETICIÓN 1 PETICIÓN 2
GET /pagina.php GET /pagina.php
↓ ↓
PHP comienza PHP comienza de nuevo
↓ ↓
$contador = 0 $contador = 0
↓ ↓
respuesta respuesta
↓ ↓
FIN FIN
Para recordar información entre peticiones necesitamos mecanismos específicos.
14.2. Primero: cookies
Una cookie es un pequeño dato que el navegador conserva y puede enviar en peticiones posteriores.
RESPUESTA
Set-Cookie: tema=oscuro
↓
NAVEGADOR
guarda tema=oscuro
↓
PETICIÓN POSTERIOR
Cookie: tema=oscuro
↓
PHP
$_COOKIE['tema']
Crear una cookie:
<?php
setcookie('tema', 'oscuro', [
'expires' => time() + 3600,
'path' => '/',
'httponly' => true,
'samesite' => 'Lax',
]);
setcookie() prepara una cabecera HTTP de respuesta. Por eso no debemos pensar que la nueva cookie aparecerá inmediatamente en $_COOKIE durante la misma ejecución: normalmente PHP la verá cuando el navegador la devuelva en una petición posterior.
Orden importante:
setcookie(),header()y el inicio de una sesión que necesite enviar su cookie deben ejecutarse antes de haber enviado contenido al navegador. Si apareceheaders already sent, busca primero HTML,echo, espacios o saltos de línea enviados demasiado pronto.
14.3. Opciones importantes de una cookie
| Opción | Idea |
|---|---|
expires |
momento de caducidad |
path |
rutas en las que se enviará |
domain |
dominio al que pertenece |
secure |
solo se envía mediante HTTPS |
httponly |
JavaScript no puede leerla mediante las API habituales |
samesite |
limita determinados envíos entre sitios |
En un laboratorio HTTP local no podemos exigir siempre secure=true; en una web pública bajo HTTPS sí debe valorarse.
14.4. Una cookie procede del cliente: validar antes de utilizar
El usuario puede inspeccionar o modificar sus cookies. Por ello una preferencia debe validarse igual que otros datos externos:
<?php
$permitidos = ['claro', 'oscuro'];
$tema = $_COOKIE['tema'] ?? 'claro';
if (
!is_string($tema)
||
!in_array($tema, $permitidos, true)
) {
$tema = 'claro';
}
Una cookie puede ser útil para preferencias no sensibles. No guardaremos contraseñas ni password_hash en cookies.
14.5. Eliminar una cookie
Pedimos al navegador que reemplace la cookie por otra equivalente con caducidad pasada:
<?php
setcookie('tema', '', [
'expires' => time() - 3600,
'path' => '/',
'httponly' => true,
'samesite' => 'Lax',
]);
El path y los atributos de alcance deben ser coherentes con los utilizados al crearla.
14.6. De la cookie a la sesión
Una sesión PHP utiliza normalmente una cookie para transportar un identificador, mientras los datos de $_SESSION se mantienen del lado servidor según el almacenamiento configurado.
NAVEGADOR
PHPSESSID=abc123
│
▼
SERVIDOR
sesión abc123
visitas = 4
user_id = 7
role = usuario
Cookie y sesión no son sinónimos:
| Aspecto | Cookie | Sesión |
|---|---|---|
| Dato principal | navegador | servidor |
| El cliente puede modificarlo directamente | sí | no directamente |
| Uso típico | preferencias / identificadores | estado de aplicación |
| Contraseña | no | tampoco |
14.7. session_start() y $_SESSION
Antes de iniciar una sesión debemos conocer también la política de su cookie. En un sitio servido exclusivamente mediante HTTPS podríamos preparar:
<?php
ini_set('session.use_strict_mode', '1');
session_set_cookie_params([
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();
En un laboratorio que todavía utilice HTTP, secure => true impediría que el navegador devolviera esa cookie por HTTP. No desactives seguridad por costumbre: adapta este atributo al entorno y exige HTTPS cuando la aplicación pase a un escenario público.
Para crear o recuperar una sesión:
<?php
session_start();
$_SESSION['visitas']
= ($_SESSION['visitas'] ?? 0) + 1;
echo $_SESSION['visitas'];
Cada script que necesite los datos de esa sesión debe iniciarla antes de utilizarlos.
14.8. Compartir datos entre páginas
guardar.php:
<?php
session_start();
$_SESSION['modulo'] = 'IAW';
echo '<a href="consultar.php">Consultar</a>';
consultar.php:
<?php
session_start();
$modulo = $_SESSION['modulo'] ?? 'Sin módulo';
echo htmlspecialchars(
is_string($modulo)
? $modulo
: 'Sin módulo'
);
Prueba la misma secuencia en una ventana privada: normalmente utilizará un contexto de cookies diferente.
14.9. Diagnosticar una sesión
Podemos observar:
<?php
session_start();
var_dump([
'name' => session_name(),
'id' => session_id(),
'status' => session_status(),
'cookie' => session_get_cookie_params(),
]);
El objetivo es comprender el mecanismo, no manipular manualmente IDs.
14.10. Duración: cookie, datos de sesión y caché son conceptos distintos
No debemos mezclar:
| Concepto | Qué controla |
|---|---|
| cookie de sesión | cuánto conserva el navegador el identificador |
| datos de sesión del servidor | ciclo de vida según almacenamiento/configuración |
| caché HTTP | cuánto puede almacenarse una respuesta en caché |
| timeout de la aplicación | regla creada por nuestro código |
Por ejemplo, una política explícita de inactividad:
<?php
const MAX_INACTIVIDAD = 900;
$ahora = time();
if (
isset($_SESSION['ultima_actividad'])
&&
$ahora - (int) $_SESSION['ultima_actividad']
> MAX_INACTIVIDAD
) {
$_SESSION = [];
session_destroy();
exit('Sesión cerrada por inactividad');
}
$_SESSION['ultima_actividad'] = $ahora;
14.11. Sesión, autenticación y autorización no son lo mismo
| Concepto | Pregunta |
|---|---|
| Sesión | ¿cómo conservamos estado? |
| Autenticación | ¿quién eres? |
| Autorización | ¿puedes ejecutar esta acción? |
Una sesión puede existir sin que nadie haya iniciado sesión.
Después de verificar unas credenciales:
<?php
session_start();
session_regenerate_id();
$_SESSION['user'] = [
'id' => 7,
'username' => 'ana',
'role' => 'usuario',
];
No necesitamos guardar contraseña ni hash en $_SESSION.
La regeneración se realiza después de verificar las credenciales y antes de guardar el estado autenticado. En este ejemplo no utilizamos session_regenerate_id(true): borrar inmediatamente los datos asociados al identificador anterior puede provocar problemas en peticiones concurrentes o conexiones inestables. La política completa de caducidad se controla con timestamps y configuración de sesión, no únicamente destruyendo IDs.
14.12. Modularizar autenticación
En lugar de repetir la misma comprobación en cada página podemos crear src/auth.php:
<?php
function currentUser(): ?array
{
$user = $_SESSION['user'] ?? null;
return is_array($user)
? $user
: null;
}
function requireLogin(): void
{
if (currentUser() === null) {
header('Location: /login.php', true, 303);
exit;
}
}
function requireRole(string $role): void
{
requireLogin();
if ((currentUser()['role'] ?? null) !== $role) {
http_response_code(403);
exit('Acceso denegado');
}
}
Una página privada podrá limitarse a:
session_start();
require_once __DIR__ . '/../src/auth.php';
requireLogin();
y una página administrativa:
requireRole('admin');
Esto conecta directamente con la programación modular estudiada anteriormente.
14.13. Ocultar un botón no es autorización
Podemos ocultar el enlace de administración a un usuario normal, pero alguien puede escribir la URL manualmente. La comprobación de permisos debe ejecutarse en el servidor.
Más adelante también comprobaremos la propiedad del recurso:
usuario autenticado
↓
rol
↓
owner_id del registro
↓
¿puede modificar ESTE recurso?
14.14. Logout completo
Un cierre de sesión debe distinguir datos, cookie identificadora y almacenamiento de sesión:
<?php
session_start();
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$params = session_get_cookie_params();
setcookie(session_name(), '', [
'expires' => time() - 42000,
'path' => $params['path'],
'domain' => $params['domain'],
'secure' => $params['secure'],
'httponly' => $params['httponly'],
'samesite' => $params['samesite'] ?? 'Lax',
]);
}
session_destroy();
header('Location: /login.php', true, 303);
exit;
14.15. Seguridad básica de la cookie de sesión
Debemos reconocer conceptos como:
HttpOnly;Securebajo HTTPS;SameSite;session.use_only_cookies;session.use_strict_mode.
No se trata de memorizar directivas; hay que comprender qué riesgo pretende reducir cada una.
14.16. Primer contacto con CSRF
Las sesiones también nos permiten guardar un token que viaje oculto en formularios de modificación:
<?php
session_start();
if (!isset($_SESSION['csrf'])) {
$_SESSION['csrf']
= bin2hex(random_bytes(32));
}
Formulario:
<input
type="hidden"
name="csrf"
value="<?= htmlspecialchars($_SESSION['csrf']) ?>"
>
Al recibir POST:
$tokenSesion = $_SESSION['csrf'] ?? null;
$tokenRecibido = $_POST['csrf'] ?? null;
if (
!is_string($tokenSesion)
|| !is_string($tokenRecibido)
|| !hash_equals($tokenSesion, $tokenRecibido)
) {
http_response_code(403);
exit('Token CSRF incorrecto');
}
Lo desarrollaremos de nuevo cuando las operaciones CRUD modifiquen datos persistentes.
14.17. Puente hacia PDO y MariaDB
Ahora los usuarios de demostración pueden estar definidos temporalmente en PHP. Con PDO cambia la fuente de los datos, no el concepto:
POST login
↓
SELECT usuario con PDO
↓
password_verify()
↓
session_regenerate_id()
↓
$_SESSION['user']
↓
autorización
15. Correo electrónico desde PHP con PHPMailer
El correo electrónico es una funcionalidad complementaria. No sustituye a la persistencia ni debe bloquear el aprendizaje del Proyecto 1. Lo utilizaremos para comprender cómo una aplicación PHP se integra con otro servicio.
15.1. Primero: separar preparación de mensaje y transporte
Antes de hablar de SMTP debemos poder:
- recibir un formulario;
- validar nombre, dirección, asunto y mensaje;
- construir el contenido;
- registrar errores;
- solo entonces intentar el envío.
Durante la explicación podemos simular el envío escribiendo un log:
<?php
$linea =
date('c')
. ' | correo preparado'
. ' | '
. $email
. PHP_EOL;
file_put_contents(
__DIR__ . '/../var/logs/mail.log',
$linea,
FILE_APPEND | LOCK_EX
);
Así distinguimos la lógica de nuestra aplicación de los problemas de red, DNS, TLS, autenticación SMTP o disponibilidad del proveedor.
15.2. PHPMailer como biblioteca
Para trabajar con una biblioteca mantenida utilizaremos Composer. Primero comprueba que el comando existe:
composer --version
Desde la raíz del proyecto:
composer require phpmailer/phpmailer
composer show phpmailer/phpmailer
Composer crea o actualiza composer.json, composer.lock y el directorio vendor/. En una aplicación conviene conservar composer.json y composer.lock en el repositorio y regenerar dependencias mediante composer install; vendor/ es código generado por Composer y normalmente no es necesario versionarlo.
Cargaremos las clases mediante el autoloader:
<?php
use PHPMailer\PHPMailer\Exception;
use PHPMailer\PHPMailer\PHPMailer;
require __DIR__ . '/../vendor/autoload.php';
$mail = new PHPMailer(true);
Aquí reaparecen conceptos de POO: PHPMailer es una clase, $mail es un objeto y isSMTP(), addAddress() o send() son métodos. El argumento true hace que PHPMailer pueda lanzar excepciones que después controlaremos.
Si Composer todavía no está preparado en el laboratorio, el envío real queda como ampliación. No descargues copias aleatorias de la biblioteca ni incrustes clases manualmente sin comprender su procedencia.
15.3. Las credenciales no pertenecen al código
No escribiremos en el repositorio:
$mail->Password = 'mi-clave-real';
La aplicación leerá configuración externa:
$host = getenv('SMTP_HOST');
$user = getenv('SMTP_USER');
$pass = getenv('SMTP_PASS');
El mismo principio se reutilizará para credenciales de MariaDB.
15.4. Ejemplo de transporte SMTP
Antes de crear el objeto, comprobamos que la configuración mínima existe. No queremos descubrir un false de getenv() varias líneas después:
<?php
use PHPMailer\PHPMailer\Exception;
use PHPMailer\PHPMailer\PHPMailer;
require __DIR__ . '/../vendor/autoload.php';
$host = getenv('SMTP_HOST');
$user = getenv('SMTP_USER');
$pass = getenv('SMTP_PASS');
$from = getenv('SMTP_FROM');
$to = getenv('SMTP_TO');
if (
!is_string($host) || $host === ''
|| !is_string($user) || $user === ''
|| !is_string($pass) || $pass === ''
|| !is_string($from) || $from === ''
|| !is_string($to) || $to === ''
) {
throw new RuntimeException(
'Falta configuración SMTP'
);
}
$mail = new PHPMailer(true);
try {
$mail->CharSet = 'UTF-8';
$mail->isSMTP();
$mail->Host = $host;
$mail->SMTPAuth = true;
$mail->Username = $user;
$mail->Password = $pass;
$mail->SMTPSecure =
PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port =
(int) (getenv('SMTP_PORT') ?: 587);
$mail->setFrom(
$from,
'IAW · Aplicación'
);
$mail->addAddress($to);
$mail->isHTML(true);
$mail->Subject = 'Prueba IAW';
$mail->Body = '<p>Mensaje HTML</p>';
$mail->AltBody = 'Mensaje en texto plano';
$mail->send();
echo 'Correo enviado';
} catch (Exception $e) {
error_log(
'IAW Desk: fallo durante el envío SMTP'
);
http_response_code(502);
echo 'No se pudo enviar la notificación';
}
El ejemplo utiliza STARTTLS y el puerto 587 porque es una configuración habitual, no una regla universal. Utiliza siempre los parámetros del servidor SMTP de laboratorio o del proveedor. No desactives la verificación TLS como forma de «arreglar» un error de certificados.
En el laboratorio se utilizarán únicamente credenciales proporcionadas para la práctica o un servidor de pruebas. No utilizaremos contraseñas personales incrustadas en el proyecto.
15.5. Adjuntos: validar antes de enviar
Un fichero recibido no se convierte automáticamente en adjunto válido. Primero aplicaremos las mismas comprobaciones de subida segura:
error
↓
tamaño
↓
MIME real
↓
tipo permitido
↓
añadir a PHPMailer
Una notificación por correo es una consecuencia de una operación; el dato principal seguirá persistiendo en la aplicación. En el Proyecto 1, un fallo de SMTP no debe deshacer silenciosamente una operación de negocio que ya se confirmó en la base de datos. La aplicación debe decidir y documentar qué ocurre si la notificación falla.
15.6. Errores
Ante un fallo de correo:
usuario
→ mensaje comprensible
log
→ detalle técnico
No mostraremos contraseñas, rutas sensibles ni configuración SMTP.
15.7. Qué debes ser capaz de explicar
- diferencia entre preparar un mensaje y transportarlo por SMTP;
- por qué usamos una biblioteca;
- relación entre PHPMailer y POO;
- por qué las credenciales deben estar fuera del código;
- por qué un adjunto debe validarse;
- por qué el fallo del correo no debe corromper los datos principales de la aplicación.
16. Banco de ejercicios progresivos
Los ejemplos resueltos de los apartados anteriores muestran cómo razonar cada construcción. Los ejercicios siguientes permiten practicar sin limitarse a copiar el ejemplo: primero realizan una modificación pequeña, después plantean una variación y finalmente integran varias técnicas conocidas.
Regla de personalización. Donde se indique NN, utiliza tu número de puesto con dos cifras: 07, 12, 25… Por ejemplo, el ticket del puesto 07 se identifica como IAW-07. Esta variable modifica los datos de prueba, no la dificultad del ejercicio. No incluyas contraseñas reales ni datos personales de terceras personas en los repositorios.
Bloque A · Primeros programas, variables y operadores
- Crea
01_saludo.phpe imprime una presentación utilizandoechoy una etiqueta<h1>. - Declara nombre, módulo, puesto y edad como variables; muestra una frase completa.
- Define dos números distintos y muestra suma, resta, producto y división si el segundo no es cero.
- Calcula el importe de una intervención con horas y tarifa; añade un IVA constante.
- Calcula la media aritmética de tres notas y muéstrala con dos decimales.
- Convierte una cantidad de segundos a minutos y segundos restantes mediante
/,intdiv()y%; compara sus resultados. - Comprueba con
var_dumpqué diferencias hay entre'8',8,8.0ytrue. - Define una cadena con el código
IAW-NN; concatena descripción y prioridad de una incidencia.
Bloque B · Condiciones
- Con
ifsimple muestra un aviso si la temperatura supera un límite. - Amplía el ejercicio anterior a
if/elsepara informar en ambos casos. - Clasifica una nota en suspenso, aprobado, notable o sobresaliente mediante
elseif. - Calcula el recargo de horas extraordinarias solo cuando se supera un límite fijado.
- Comprueba con
&&si el número pertenece a un rango y con||si corresponde a uno de dos casos especiales. - Construye una calculadora que no divida por cero.
- Reescribe una clasificación de prioridad usando
switch, una vez que funcione medianteelseif.
Bloque C · Repeticiones y colecciones
- Genera los números del 1 hasta
NNmediantefor. - Muestra los múltiplos de tres sin utilizar HTML repetido a mano.
- Calcula el factorial de un entero pequeño mediante un acumulador.
- Genera la tabla de multiplicar de tu número de puesto.
- Crea un array de notas, recórrelo con
foreachy muestra la media. - Crea un array asociativo con los datos de un ticket
IAW-NN. - Construye un array con varios tickets y cuenta cuántos siguen abiertos.
- Genera una tabla HTML a partir de ese array sin copiar y pegar filas.
- Identifica en un código dado un bucle que no termina y explica qué variable debe actualizarse.
Bloque D · Funciones, modularidad y ficheros
- Implementa
esPar(int $n): booly pruébala con valores pares e impares. - Implementa
calcularImporte(float $horas, float $tarifa): float. - Implementa
contarAbiertos(array $tickets): int. - Separa dos funciones en
src/funciones.phpy utilízalas desdepublic/index.php. - Extrae un fragmento repetido de HTML a
templates/cabecera.php. - Crea
var/logs/app.logy añade una línea con fecha, códigoIAW-NNy una acción de prueba. - Añade varias líneas al log utilizando
FILE_APPEND | LOCK_EXy demuestra que no se borra el contenido anterior. - Lee el log y muéstralo dentro de
<pre>escapando su contenido. - Guarda un pequeño array de tickets en
var/data/tickets.jsony recupéralo en otra ejecución. - Explica qué limitaciones tendría ese JSON si veinte usuarios intentasen actualizar tickets simultáneamente.
Bloque E · Primeros objetos
- Crea la clase
Ticket; instancia dos objetos con asuntos diferentes. - Añade un método que cierre un ticket sin dejar modificar libremente su estado.
- Añade un constructor y demuestra que dos objetos conservan valores independientes.
- Explica mediante un esquema qué responsabilidad corresponde a cada archivo, fichero de datos o clase.
Bloque F · Formularios y estado
- Crea
formulario.phpyprocesar.php: el primero muestra un campoasuntoy el segundo recibe el POST. - Convierte el ejercicio anterior en un único
formulario.phpque muestre y procese los datos según$_SERVER['REQUEST_METHOD']. - Dibuja las peticiones HTTP de ambos ejercicios y explica por qué «mismo fichero» no significa «misma petición».
- Conserva el valor escrito en el formulario cuando la validación detecte un error.
- Valida en PHP que el asunto no sea una cadena vacía ni un array.
- Añade prioridad y comprueba que pertenezca a la lista permitida.
- Realiza una petición GET de búsqueda a la misma página y compara sus datos con el POST de creación.
- Recibe un
input type="number", demuestra convar_dumpqué llega y valida el entero en servidor. - Muestra un asunto que contenga
<b>y comprueba que aparece como texto mediantehtmlspecialchars. - Extrae la normalización y validación del asunto a funciones propias.
- Registra en un log una operación de formulario válida sin guardar datos sensibles.
- Crea un contador de sesión independiente de un contador local reiniciado en cada petición.
- Guarda y recupera una preferencia no sensible mediante cookie.
- Integra un formulario validado y una página de confirmación sin almacenar todavía el ticket en MariaDB.
Antes de entregar o publicar una aplicación: comprueba al menos el caso esperado, el caso incorrecto y un valor límite; explica qué ocurre y por qué. No es suficiente obtener una captura de pantalla de un resultado casualmente correcto.
Bloque G · Seguridad, correo y ficheros seguros
- Manipula manualmente una cookie de preferencia y demuestra por qué el servidor debe validar también su contenido.
- Implementa un login temporal separado en
login.php,panel.phpysrc/auth.php; protege una página conrequireLogin()y otra conrequireRole('admin'). - Añade logout y un timeout de inactividad. Explica qué problema resuelve cada mecanismo.
- Añade un token CSRF a un formulario POST e intenta enviarlo después de modificar el token desde las herramientas del navegador.
- Configura las opciones de la cookie de sesión y justifica
HttpOnly,SameSitey el uso deSecurecuando exista HTTPS. - Crea un formulario de contacto que valide los datos y simule el correo mediante un log. Como ampliación, sustituye el simulador por PHPMailer y SMTP de laboratorio.
- Sube un PDF/JPEG/PNG validando error, tamaño y MIME real; genera un nombre interno aleatorio y almacénalo fuera de
public/. - Diseña una descarga controlada que reciba un ID lógico y nunca una ruta arbitraria proporcionada por el cliente.
- Explica qué información de cookies, sesiones y ficheros permanecerá cuando el proyecto migre a MariaDB y qué información pasará a la base de datos.
17. Diagnóstico y buenas prácticas
Los errores proporcionan información sobre qué capa está fallando. No empieces a modificar PHP si todavía no has comprobado que la página correcta llega al servidor.
| Síntoma | Primera comprobación |
|---|---|
| No conecta por SSH | IP de Debian, red, servidor SSH y credenciales. |
| El navegador no accede al sitio | IP, DNS/hosts, Apache y VirtualHost. |
| Aparece el PHP como texto | La integración con PHP-FPM no se está aplicando correctamente. Suspende la publicación. |
| HTTP 404 | Nombre de archivo, ruta y DocumentRoot. |
| HTTP 500 | php -l, registro de error de Apache y registro de PHP-FPM. |
Undefined array key |
Una clave que estás consultando no existe en el array. |
TypeError |
Una función recibió un tipo no admitido o una operación no puede realizarse con ese dato. |
| El resultado numérico es incorrecto | Comprueba asignaciones, precedencia y tipos con una traza. |
| El bucle no termina | Busca la condición de salida y la variable que debería actualizarse. |
| Los datos desaparecen en otra página | Revisa primero qué se conserva por petición y qué se ha guardado en sesión. |
headers already sent |
Busca salida enviada antes de header(), setcookie() o del inicio de sesión. |
| La sesión desaparece | Comprueba session_start(), cookie de sesión, path, Secure, SameSite y timeout. |
| La subida falla | Revisa $_FILES['error'], límites PHP, tamaño, MIME, ruta y permisos de var/uploads/. |
| El correo no sale | Separa primero lógica de aplicación de DNS, puerto, TLS, autenticación y disponibilidad SMTP. |
Comandos básicos desde la terminal remota:
php -l public/01_hola.php
php -v
sudo apache2ctl configtest
sudo systemctl status apache2
sudo systemctl status php8.4-fpm
sudo tail -n 30 /var/log/apache2/error.log
Los nombres de servicio y las rutas de registros pueden variar según la instalación y el VirtualHost. No necesitas ejecutar todos los comandos ante cualquier error: selecciona la prueba que corresponda al síntoma.
Reglas que conservaremos en las siguientes unidades: utilizar variables descriptivas; comprobar casos límite; separar cálculos de presentación cuando aporte claridad; no confiar en datos del navegador; no publicar secretos; evitar editar como root o dar permisos 777; escapar contenido no confiable al generar HTML; y comprender la responsabilidad de cada servicio antes de reiniciarlo.
18. Glosario y referencias
| Término | Significado breve |
|---|---|
| Intérprete | Programa que ejecuta instrucciones PHP. |
| CLI | Ejecución desde la terminal. |
| PHP-FPM | Gestor de procesos que ejecuta PHP en el servicio web. |
| Variable | Identificador al que asignamos un valor. |
| Expresión | Combinación de datos y operadores que produce un resultado. |
| Booleano | Valor verdadero o falso. |
| Condición | Expresión que permite decidir qué bloque ejecutar. |
| Bucle | Estructura que repite instrucciones. |
| Array | Colección de valores accesibles mediante claves. |
| Función | Bloque reutilizable que puede recibir argumentos y devolver un valor. |
| Clase / objeto | Modelo y una instancia concreta de ese modelo. |
| GET / POST | Métodos de petición HTTP con usos distintos. |
| Cookie | Dato gestionado por el navegador y enviado bajo ciertas condiciones. |
| Fichero persistente | Archivo del servidor que conserva información entre ejecuciones mientras permanezca el almacenamiento. |
| Log | Registro cronológico de eventos o errores utilizado para operación y diagnóstico. |
| JSON | Formato textual estructurado que puede representar arrays y objetos simples. |
| Sesión | Estado asociado a peticiones relacionadas, administrado por el servidor. |
| Función incorporada | Función proporcionada por PHP que reutilizamos conociendo parámetros, retorno y casos límite. |
| Callback | Función o callable entregado a otra operación para que pueda ejecutarlo. |
| Autenticación | Proceso de comprobar la identidad de un usuario. |
| Autorización | Decisión sobre si una identidad puede realizar una acción concreta. |
| CSRF | Ataque que intenta provocar una acción autenticada desde una petición no deseada; se mitiga, entre otras medidas, con tokens y políticas de cookie. |
| MIME | Tipo de contenido utilizado para identificar la naturaleza de datos o ficheros; en subidas debe comprobarse del lado servidor. |
| Composer | Gestor de dependencias utilizado para instalar y reproducir bibliotecas PHP. |
| SMTP | Protocolo utilizado para transportar correo electrónico entre sistemas. |
Documentación para ampliar y contrastar ejemplos:
- Manual oficial de PHP: introducción y sintaxis.
- Variables, tipos de datos y operadores, tipos y operadores.
- Estructuras de control de PHP.
- Arrays y
foreach. - Funciones y clases/objetos.
- Superglobales,
htmlspecialcharsy sesiones. - PHP: seguridad de sesiones,
setcookie()ysession_regenerate_id(). - PHP: subida de ficheros,
move_uploaded_file()y Fileinfo. - Composer: uso básico.
- PHPMailer: repositorio y documentación oficial.
- VS Code Remote - SSH.
- OWASP: prevención de XSS.
- OWASP: prevención de CSRF.
Continuación: UT03 · PHP y bases de datos. Tras dominar variables, control de flujo, funciones, formularios y estado, incorporaremos PDO y MariaDB para conservar datos de manera estructurada y duradera.