Módulo 06: Cadenas de texto
Objetivos del módulo
- Elegir entre comillas simples y dobles y dominar la interpolación de variables
- Usar secuencias de escape, heredoc y nowdoc
- Manejar las funciones de cadenas más importantes
- Trabajar correctamente con UTF-8 y las funciones multibyte (
mb_*), incluidas las novedades de PHP 8.3 y 8.4 - Dar formato a textos y números con
printf(),sprintf()ynumber_format() - Comparar cadenas y medir su parecido
- Buscar, validar y transformar texto con expresiones regulares PCRE
- Escapar la salida HTML con
htmlspecialchars() - Mostrar fechas con
date()yDateTimeImmutable
1. Comillas simples y dobles
📘 Concepto: Una cadena (string) es una secuencia de caracteres: una palabra, una frase, el contenido de un fichero o un documento HTML completo. En PHP se escriben entre comillas simples
'...'o dobles"...", y no se comportan igual.
| Característica | Comillas simples '...' | Comillas dobles "..." |
|---|---|---|
| Interpolan variables | ❌ No | ✅ Sí |
| Secuencias de escape | Solo \' y \\ | Todas (\n, \t, \$, \u{...}…) |
| Uso recomendado | Texto literal, claves de arrays | Texto con variables o saltos de línea |
<?php
declare(strict_types=1);
$producto = 'portátil';
echo 'Has comprado un $producto\n'; // Has comprado un $producto\n (literal)
echo PHP_EOL;
echo "Has comprado un $producto\n"; // Has comprado un portátil (con salto de línea)
💡 Consejo: Una convención muy extendida es usar comillas simples por defecto y dobles solo cuando necesitas interpolar o escapar. Así, quien lea el código sabe de un vistazo si la cadena contiene algo “dinámico”.
2. Interpolación de variables
Dentro de comillas dobles (y de heredoc), PHP sustituye las variables por su valor.
Sintaxis simple
<?php
declare(strict_types=1);
$nombre = 'Inés';
$edad = 31;
$colores = ['rojo', 'verde'];
$usuario = ['email' => 'ines@ejemplo.es'];
echo "Me llamo $nombre y tengo $edad años" . PHP_EOL;
echo "Mi color favorito es el $colores[0]" . PHP_EOL; // Índice numérico
echo "Mi email es $usuario[email]" . PHP_EOL; // Clave SIN comillas (solo en sintaxis simple)
Sintaxis con llaves {$...}
Las llaves delimitan exactamente dónde empieza y termina la expresión. Es la forma más clara y la única que admite claves con comillas, propiedades encadenadas y llamadas a métodos:
<?php
declare(strict_types=1);
$fruta = 'manzana';
$pedido = ['cliente' => ['nombre' => 'Óscar'], 'total' => 42.5];
$fecha = new DateTimeImmutable('2026-10-03');
echo "Me gustan las {$fruta}s" . PHP_EOL; // manzanas
echo "Cliente: {$pedido['cliente']['nombre']}" . PHP_EOL; // Óscar
echo "Total: {$pedido['total']} €" . PHP_EOL; // 42.5 €
echo "Fecha: {$fecha->format('d/m/Y')}" . PHP_EOL; // 03/10/2026
⚠️ Importante: Sin llaves,
"Me gustan las $frutas"buscaría una variable llamada$frutas. Las llaves resuelven la ambigüedad:"{$fruta}s".
| Sintaxis | Ejemplo | ¿Recomendada? |
|---|---|---|
| Simple | "Hola $nombre" | ✅ Para variables sueltas |
| Simple con array | "$datos[clave]" | ⚠️ Funciona, pero confunde (clave sin comillas) |
| Llaves | "{$datos['clave']}" | ✅ Para arrays, objetos y métodos |
${...} | "${nombre}" | ❌ Obsoleta desde PHP 8.2 |
💡 Consejo: La interpolación solo funciona con variables, no con funciones o constantes:
"Total: {strtoupper($x)}"no funciona. Para eso, concatena o usasprintf()(sección 7).
3. Secuencias de escape
Dentro de comillas dobles, la barra invertida \ introduce caracteres especiales:
| Secuencia | Significado |
|---|---|
\n | Salto de línea |
\r | Retorno de carro |
\t | Tabulador |
\\ | Barra invertida literal |
\$ | Signo de dólar literal (evita la interpolación) |
\" | Comilla doble |
\u{1F418} | Carácter Unicode por su código (aquí, 🐘, la mascota de PHP) |
\x41 | Carácter por su código hexadecimal (A) |
\0 | Carácter nulo |
<?php
echo "Nombre:\tAna\nCiudad:\tLeón\n";
// Nombre: Ana
// Ciudad: León
echo "El precio es \$5\n"; // El precio es $5
echo "Ruta: C:\\php\\ext\n"; // Ruta: C:\php\ext
echo "PHP \u{1F418} 8.5\n"; // PHP 🐘 8.5
echo 'En simples, \n no es un salto'; // En simples, \n no es un salto
4. Heredoc y nowdoc
Para textos largos de varias líneas (plantillas de correo, HTML, SQL), las comillas se vuelven incómodas. PHP ofrece dos sintaxis especiales.
Heredoc: como comillas dobles
<?php
declare(strict_types=1);
$cliente = 'Marcos';
$pedido = ['numero' => 'A-1024', 'total' => 89.90];
$correo = <<<TEXTO
Hola, $cliente:
Tu pedido {$pedido['numero']} se ha enviado.
Importe total: {$pedido['total']} €
Gracias por confiar en nosotros.
TEXTO;
echo $correo;
Hola, Marcos:
Tu pedido A-1024 se ha enviado.
Importe total: 89.9 €
Gracias por confiar en nosotros.
- Empieza con
<<<IDENTIFICADOR(el nombre lo eliges tú:TEXTO,HTML,SQL,EOT…). - Termina con el mismo identificador en su propia línea.
- Interpola variables y procesa secuencias de escape, igual que las comillas dobles.
📘 Concepto: Desde PHP 7.3, el identificador de cierre puede estar sangrado, y PHP elimina esa misma cantidad de espacios de todas las líneas. Por eso en el ejemplo el texto resultante no tiene sangría. Si alguna línea tiene menos sangría que el cierre, se produce un error.
Nowdoc: como comillas simples
Si pones el identificador entre comillas simples, obtienes un nowdoc: el texto se toma literalmente, sin interpolación. Ideal para código de ejemplo o plantillas con $:
<?php
$plantilla = <<<'CODIGO'
<?php
$precio = 10;
echo "Total: $precio €\n";
CODIGO;
echo $plantilla; // Muestra el código tal cual, con $precio sin sustituir
5. Concatenación
Concatenar con . y .=
<?php
declare(strict_types=1);
$nombre = 'Lucía';
$apellidos = 'Fernández Ruiz';
$completo = $nombre . ' ' . $apellidos;
$lista = '<ul>';
foreach (['HTML', 'CSS', 'PHP'] as $tecnologia) {
$lista .= "<li>$tecnologia</li>";
}
$lista .= '</ul>';
echo $completo . PHP_EOL; // Lucía Fernández Ruiz
echo $lista . PHP_EOL; // <ul><li>HTML</li><li>CSS</li><li>PHP</li></ul>
💡 Consejo: Para unir muchos fragmentos, suele ser más limpio guardarlos en un array y unirlos al final con
implode():implode(PHP_EOL, $lineas).
6. Funciones esenciales de cadenas
| Función | Qué hace | Ejemplo | Resultado |
|---|---|---|---|
strlen($s) | Longitud en bytes | strlen('Hola') | 4 |
str_contains($s, $buscar) | ¿Contiene? (PHP 8.0) | str_contains('Hola mundo', 'mun') | true |
str_starts_with($s, $ini) | ¿Empieza por? (PHP 8.0) | str_starts_with('index.php', 'index') | true |
str_ends_with($s, $fin) | ¿Termina en? (PHP 8.0) | str_ends_with('foto.jpg', '.jpg') | true |
strpos($s, $buscar) | Posición de la primera aparición (o false) | strpos('banana', 'n') | 2 |
stripos($s, $buscar) | Igual, sin distinguir mayúsculas | stripos('HOLA', 'o') | 1 |
strrpos($s, $buscar) | Posición de la última aparición | strrpos('banana', 'a') | 5 |
substr($s, $ini, $long) | Extrae una parte | substr('Programar', 0, 4) | 'Prog' |
substr_count($s, $buscar) | Cuántas veces aparece | substr_count('banana', 'a') | 3 |
str_replace($buscar, $poner, $s) | Reemplaza | str_replace('a', 'o', 'casa') | 'coso' |
str_ireplace(...) | Reemplaza sin distinguir mayúsculas | str_ireplace('HOLA', 'Adiós', 'hola') | 'Adiós' |
strtolower($s) / strtoupper($s) | Minúsculas / mayúsculas (solo ASCII) | strtoupper('php') | 'PHP' |
ucfirst($s) / lcfirst($s) | Primera letra mayúscula / minúscula | ucfirst('hola') | 'Hola' |
ucwords($s) | Primera letra de cada palabra en mayúscula | ucwords('juan pérez') | 'Juan Pérez' |
trim($s) | Quita espacios al principio y al final | trim(' hola ') | 'hola' |
ltrim($s) / rtrim($s) | Solo por la izquierda / derecha | ltrim('0012', '0') | '12' |
str_pad($s, $long, $relleno, $lado) | Rellena hasta una longitud | str_pad('5', 3, '0', STR_PAD_LEFT) | '005' |
str_repeat($s, $n) | Repite | str_repeat('-=', 3) | '-=-=-=' |
explode($sep, $s) | Divide en un array | explode(',', 'a,b,c') | ['a', 'b', 'c'] |
implode($sep, $array) | Une un array en una cadena | implode(' \| ', ['x', 'y']) | 'x \| y' |
nl2br($s) | Convierte saltos de línea en <br /> | nl2br("a\nb") | "a<br />\nb" |
wordwrap($s, $ancho, $corte, true) | Corta el texto en líneas | ver ejemplo | — |
strrev($s) | Invierte (por bytes) | strrev('hola') | 'aloh' |
strtr($s, $pares) | Traduce varias subcadenas a la vez | strtr('Hola', ['Hola' => 'Adiós']) | 'Adiós' |
Buscar texto: str_contains() y la trampa de strpos()
<?php
declare(strict_types=1);
$email = 'admin@miempresa.es';
// PHP 8: claro y sin trampas
if (str_contains($email, '@')) {
echo 'Tiene arroba' . PHP_EOL;
}
if (str_ends_with($email, '.es')) {
echo 'Dominio español' . PHP_EOL;
}
// ⚠️ La trampa clásica de strpos
$texto = 'PHP es genial';
if (strpos($texto, 'PHP')) { // ❌ strpos devuelve 0 (posición), que es "falso"
echo 'Esto NO se muestra';
}
if (strpos($texto, 'PHP') !== false) { // ✅ Comparación estricta con false
echo 'Encontrado' . PHP_EOL;
}
💡 Consejo: Para preguntar “¿contiene?”, usa siempre
str_contains(),str_starts_with()ostr_ends_with(). Reservastrpos()para cuando necesitas la posición.
Reemplazar: str_replace()
<?php
declare(strict_types=1);
$plantilla = 'Hola {nombre}, tu código es {codigo}.';
// Arrays de búsqueda y reemplazo (se emparejan por posición)
$mensaje = str_replace(
['{nombre}', '{codigo}'],
['Teresa', 'XK-55'],
$plantilla
);
echo $mensaje . PHP_EOL; // Hola Teresa, tu código es XK-55.
// El cuarto parámetro cuenta los reemplazos
$limpio = str_replace(' ', '', '6 1 2 3 4 5', $reemplazos);
echo "$limpio ($reemplazos espacios eliminados)" . PHP_EOL; // 612345 (6 espacios eliminados)
Limpiar y rellenar: trim() y str_pad()
<?php
declare(strict_types=1);
$entrada = " usuario@correo.es \n";
echo '[' . trim($entrada) . ']' . PHP_EOL; // [usuario@correo.es]
echo trim('--título--', '-') . PHP_EOL; // título (quita los caracteres indicados)
// Números de factura con ceros a la izquierda
echo str_pad('42', 6, '0', STR_PAD_LEFT) . PHP_EOL; // 000042
// Columnas alineadas en la terminal
foreach (['Pan' => 1.2, 'Leche' => 0.95, 'Aceite' => 8.5] as $producto => $precio) {
echo str_pad($producto, 10, '.') . str_pad((string) $precio, 6, ' ', STR_PAD_LEFT) . PHP_EOL;
}
// Pan....... 1.2
// Leche..... 0.95
// Aceite.... 8.5
💡 Consejo: Valida y limpia siempre con
trim()los datos que llegan de formularios. Un usuario que escribe" admin "no debería poder registrarse como un usuario distinto de"admin".
Dividir y unir: explode() e implode()
<?php
declare(strict_types=1);
$csv = 'Ana;García;Madrid;34';
$campos = explode(';', $csv);
print_r($campos); // [Ana, García, Madrid, 34]
[$nombre, $apellido, $ciudad] = explode(';', $csv); // Desestructuración
echo "$nombre vive en $ciudad" . PHP_EOL; // Ana vive en Madrid
// Límite: como máximo 2 trozos
[$usuario, $resto] = explode('@', 'pepe@mail.empresa.es', 2);
echo $resto . PHP_EOL; // mail.empresa.es
$etiquetas = ['php', 'backend', 'web'];
echo implode(', ', $etiquetas) . PHP_EOL; // php, backend, web
echo '#' . implode(' #', $etiquetas) . PHP_EOL; // #php #backend #web
7. Multibyte y UTF-8
El problema
📘 Concepto: Los ordenadores guardan el texto como bytes. En UTF-8, la codificación universal de la web, las letras básicas (a-z, 0-9) ocupan 1 byte, pero
á,ño€ocupan 2 o 3, y un emoji ocupa 4. Las funciones clásicas de PHP (strlen,substr,strtoupper…) trabajan con bytes, así que con texto en español pueden dar resultados incorrectos.
<?php
$palabra = 'Año';
echo strlen($palabra) . PHP_EOL; // 4 ← ¡la ñ cuenta como 2 bytes!
echo mb_strlen($palabra) . PHP_EOL; // 3 ✅
echo strtoupper('ñandú') . PHP_EOL; // ñANDú ← solo convierte letras ASCII
echo mb_strtoupper('ñandú') . PHP_EOL; // ÑANDÚ ✅
echo substr('Murciélago', 0, 6) . PHP_EOL; // Murci + medio carácter roto (�)
echo mb_substr('Murciélago', 0, 6) . PHP_EOL; // Murcié ✅
echo strrev('año') . PHP_EOL; // Bytes invertidos: texto corrupto
echo strlen('😀') . ' / ' . mb_strlen('😀') . PHP_EOL; // 4 / 1
⚠️ Importante: Desde PHP 8.2,
strtolower(),strtoupper(),ucfirst()y similares solo convierten letras ASCII, sin importar la configuración regional. Para texto con tildes y eñes, usa siempre sus versionesmb_*.
Funciones multibyte
La extensión mbstring (que activaste en el módulo 01) ofrece versiones seguras para UTF-8:
| Función clásica | Versión multibyte | Ejemplo con mb_ | Resultado |
|---|---|---|---|
strlen() | mb_strlen() | mb_strlen('camión') | 6 |
substr() | mb_substr() | mb_substr('España', 0, 4) | 'Espa' |
strpos() | mb_strpos() | mb_strpos('añoño', 'o') | 2 |
strtoupper() | mb_strtoupper() | mb_strtoupper('pingüino') | 'PINGÜINO' |
strtolower() | mb_strtolower() | mb_strtolower('ÁRBOL') | 'árbol' |
ucwords() | mb_convert_case($s, MB_CASE_TITLE) | mb_convert_case('ébano real', MB_CASE_TITLE) | 'Ébano Real' |
str_split() | mb_str_split() | mb_str_split('año') | ['a', 'ñ', 'o'] |
str_pad() | mb_str_pad() (PHP 8.3) | mb_str_pad('Año', 6, '·') | 'Año···' |
trim() | mb_trim() (PHP 8.4) | mb_trim("\u{3000}hola\u{00A0}") | 'hola' |
ltrim() / rtrim() | mb_ltrim() / mb_rtrim() (PHP 8.4) | mb_ltrim('¡¡hola', '¡') | 'hola' |
ucfirst() | mb_ucfirst() (PHP 8.4) | mb_ucfirst('ácido') | 'Ácido' |
lcfirst() | mb_lcfirst() (PHP 8.4) | mb_lcfirst('Ñandú') | 'ñandú' |
mb_str_pad() (PHP 8.3)
<?php
declare(strict_types=1);
// str_pad cuenta bytes: la columna con tilde queda desalineada
echo '[' . str_pad('Camión', 10, '.') . ']' . PHP_EOL; // [Camión...] (9 caracteres visibles)
echo '[' . mb_str_pad('Camión', 10, '.') . ']' . PHP_EOL; // [Camión....] (10) ✅
echo '[' . mb_str_pad('Coche', 10, '.') . ']' . PHP_EOL; // [Coche.....] (10) ✅
mb_trim(), mb_ltrim() y mb_rtrim() (PHP 8.4)
trim() solo elimina espacios “clásicos” (espacio, tabulador, saltos de línea…). Los textos copiados de webs o documentos de Word suelen incluir espacios Unicode invisibles, como el espacio de no separación (U+00A0), que trim() no quita:
<?php
declare(strict_types=1);
$copiado = "\u{00A0} Madrid \u{00A0}"; // Espacios de no separación
var_dump(trim($copiado)); // string(12) " Madrid " ← los U+00A0 siguen ahí
var_dump(mb_trim($copiado)); // string(6) "Madrid" ✅
// También admiten una lista de caracteres a eliminar
echo mb_ltrim('¿¿¿Qué???', '¿') . PHP_EOL; // Qué???
echo mb_rtrim('¿¿¿Qué???', '?') . PHP_EOL; // ¿¿¿Qué
mb_ucfirst() y mb_lcfirst() (PHP 8.4)
<?php
declare(strict_types=1);
echo ucfirst('ácido') . PHP_EOL; // ácido ← no cambia
echo mb_ucfirst('ácido') . PHP_EOL; // Ácido ✅
echo mb_lcfirst('Ñandú') . PHP_EOL; // ñandú ✅
// Formatear un nombre que el usuario escribió en mayúsculas
$nombre = 'ÁLVARO';
echo mb_ucfirst(mb_strtolower($nombre)) . PHP_EOL; // Álvaro
💡 Consejo: Trabaja siempre en UTF-8 de principio a fin: ficheros guardados en UTF-8,
<meta charset="UTF-8">en el HTML, la base de datos conutf8mb4(módulo 15) y funcionesmb_*cuando el texto pueda contener caracteres no ASCII. El valor por defecto dembstringen PHP moderno ya es UTF-8.
8. Formatear texto y números
printf() y sprintf()
printf() muestra un texto con marcadores que se sustituyen por valores. sprintf() hace lo mismo pero devuelve la cadena en lugar de mostrarla:
<?php
declare(strict_types=1);
$nombre = 'Ana';
$edad = 30;
$altura = 1.684;
printf("%s tiene %d años y mide %.2f m\n", $nombre, $edad, $altura);
// Ana tiene 30 años y mide 1.68 m
$mensaje = sprintf('Pedido nº %05d', 42);
echo $mensaje . PHP_EOL; // Pedido nº 00042
| Marcador | Significado | Ejemplo | Resultado |
|---|---|---|---|
%s | Cadena | sprintf('%s', 'PHP') | PHP |
%d | Entero | sprintf('%d', 42) | 42 |
%f | Decimal (6 decimales) | sprintf('%f', 3.14) | 3.140000 |
%.2f | Decimal con 2 decimales | sprintf('%.2f', 3.14159) | 3.14 |
%5d | Ancho mínimo 5 (alineado a la derecha) | sprintf('[%5d]', 42) | [ 42] |
%-5d | Ancho 5 alineado a la izquierda | sprintf('[%-5d]', 42) | [42 ] |
%05d | Rellenar con ceros | sprintf('%05d', 42) | 00042 |
%'*10s | Rellenar con un carácter propio | sprintf("%'*10s", 'PHP') | *******PHP |
%+d | Mostrar siempre el signo | sprintf('%+d', 7) | +7 |
%x / %X | Hexadecimal | sprintf('%x', 255) | ff |
%b | Binario | sprintf('%b', 5) | 101 |
%e | Notación científica | sprintf('%.2e', 12345.678) | 1.23e+4 |
%% | El símbolo % | sprintf('%d%%', 50) | 50% |
%1$s | Argumento por posición | sprintf('%2$s %1$s', 'mundo', 'Hola') | Hola mundo |
⚠️ Importante: En los formatos con
$posicional, usa comillas simples ('%1$s'), o PHP intentará interpolar$scomo variable dentro de las comillas dobles.
number_format(): números para personas
<?php
declare(strict_types=1);
$cifra = 1234567.891;
echo number_format($cifra) . PHP_EOL; // 1,234,568 (formato inglés, sin decimales)
echo number_format($cifra, 2) . PHP_EOL; // 1,234,567.89
echo number_format($cifra, 2, ',', '.') . PHP_EOL; // 1.234.567,89 ✅ formato español
echo number_format(0.5) . PHP_EOL; // 1 (redondea)
function euros(float $cantidad): string
{
return number_format($cantidad, 2, ',', '.') . ' €';
}
echo euros(1499.9) . PHP_EOL; // 1.499,90 €
💡 Consejo: Para aplicaciones multi-idioma, la extensión intl ofrece la clase
NumberFormatter, que aplica automáticamente las reglas de cada país:(new NumberFormatter('es_ES', NumberFormatter::CURRENCY))->formatCurrency(1499.9, 'EUR')produce1.499,90 €.
9. Comparar cadenas
strcmp() y strcasecmp()
Devuelven un número negativo, cero o positivo (útiles para ordenar):
| Función | Compara | Ejemplo | Resultado |
|---|---|---|---|
strcmp($a, $b) | Distinguiendo mayúsculas | strcmp('a', 'b') | -1 |
strcasecmp($a, $b) | Sin distinguir mayúsculas (ASCII) | strcasecmp('Hola', 'HOLA') | 0 |
strncmp($a, $b, $n) | Solo los $n primeros caracteres | strncmp('PHP8', 'PHP7', 3) | 0 |
strnatcmp($a, $b) | Orden “natural” | strnatcmp('img12', 'img10') | 1 |
<?php
declare(strict_types=1);
$ciudades = ['bilbao', 'Almería', 'Cádiz', 'alicante'];
usort($ciudades, strcasecmp(...)); // Orden alfabético sin distinguir mayúsculas
print_r($ciudades); // [alicante, Almería, bilbao, Cádiz]
Medir el parecido: levenshtein() y similar_text()
Útiles para sugerencias del tipo “¿Quisiste decir…?”:
<?php
declare(strict_types=1);
// levenshtein: número mínimo de cambios (insertar, borrar, sustituir) para pasar de una a otra
echo levenshtein('kitten', 'sitting') . PHP_EOL; // 3
echo levenshtein('casa', 'cosas') . PHP_EOL; // 2
// similar_text: caracteres en común y porcentaje de similitud
$comunes = similar_text('World', 'Word', $porcentaje);
echo "$comunes caracteres comunes, " . round($porcentaje, 1) . ' %' . PHP_EOL;
// 4 caracteres comunes, 88.9 %
⚠️ Importante:
levenshtein()ysimilar_text()trabajan con bytes: una letra con tilde cuenta como dos. Para comparar palabras en español con precisión, puedes normalizarlas antes (por ejemplo, quitando las tildes).
10. Expresiones regulares (PCRE)
📘 Concepto: Una expresión regular (regex) es un patrón que describe un conjunto de cadenas. Sirve para buscar, validar, extraer o reemplazar texto que sigue una forma: “cinco dígitos”, “algo@algo.algo”, “una fecha dd/mm/aaaa”. PHP usa el motor PCRE (Perl Compatible Regular Expressions), el mismo estándar que JavaScript, Python o Java.
Anatomía de un patrón
/^\d{5}$/u
│││ ││ └── Modificador: u = UTF-8
││└───┘└──── Delimitador de cierre
││ └────── Cuantificador: exactamente 5 veces
│└────────── Clase: un dígito
└─────────── Delimitador de apertura (y ^ = inicio de la cadena; $ = final)
Chuleta de sintaxis
| Elemento | Significado | Ejemplo | Coincide con |
|---|---|---|---|
. | Cualquier carácter (salvo salto de línea) | /c.sa/ | casa, cosa, c3sa |
\d | Dígito (0-9) | /\d\d/ | 42 |
\w | Letra, dígito o _ | /\w+/ | hola_123 |
\s | Espacio en blanco | /a\sb/ | “a b” |
\D, \W, \S | Lo contrario de \d, \w, \s | /\D+/ | abc |
[abc] | Uno de esos caracteres | /[aeiou]/ | una vocal |
[a-z] | Rango | /[A-Z]/ | una mayúscula |
[^abc] | Cualquiera excepto esos | /[^0-9]/ | un no dígito |
^ / $ | Inicio / final de la cadena | /^Hola$/ | exactamente “Hola” |
* | 0 o más | /ab*/ | a, ab, abbb |
+ | 1 o más | /ab+/ | ab, abbb |
? | 0 o 1 (opcional) | /colou?r/ | color, colour |
{n} / {n,m} | Exactamente n / entre n y m | /\d{2,4}/ | 12, 123, 1234 |
(...) | Grupo de captura | /(\d+)€/ | captura el número |
(?<nombre>...) | Grupo con nombre | /(?<anio>\d{4})/ | captura “anio” |
(?:...) | Grupo sin captura | /(?:ab)+/ | ab, abab |
a\|b | Alternativa | /gato\|perro/ | gato o perro |
\ | Escapar un carácter especial | /\./ | un punto literal |
| Modificador | Efecto |
|---|---|
i | No distingue mayúsculas |
u | Trata el patrón y el texto como UTF-8 (imprescindible con tildes) |
m | ^ y $ coinciden con cada línea |
x | Permite espacios y comentarios en el patrón |
preg_match(): ¿coincide?
Devuelve 1 si encuentra una coincidencia, 0 si no, y false si el patrón es erróneo. Opcionalmente, rellena un array con lo encontrado:
<?php
declare(strict_types=1);
$telefono = '91-5551234';
if (preg_match('/^(\d{2,3})-(\d{6,7})$/', $telefono, $partes) === 1) {
echo "Prefijo: {$partes[1]}, número: {$partes[2]}" . PHP_EOL;
}
// Prefijo: 91, número: 5551234
print_r($partes);
// [0 => 91-5551234, 1 => 91, 2 => 5551234] ← [0] es la coincidencia completa
Grupos con nombre
En lugar de recordar que el día es el grupo 1 y el mes el 2, dales nombre:
<?php
declare(strict_types=1);
$texto = 'La reunión es el 03/10/2026 a las 10:30';
if (preg_match('#(?<dia>\d{2})/(?<mes>\d{2})/(?<anio>\d{4})#', $texto, $fecha) === 1) {
echo "Día: {$fecha['dia']}, mes: {$fecha['mes']}, año: {$fecha['anio']}" . PHP_EOL;
}
// Día: 03, mes: 10, año: 2026
💡 Consejo: El delimitador no tiene por qué ser
/. Si el patrón contiene barras (fechas, rutas, URLs), usa#o~para no tener que escaparlas todas:#/productos/(\d+)#.
preg_match_all(): todas las coincidencias
<?php
declare(strict_types=1);
$tuit = 'Aprendiendo #PHP y #programación web en #2026';
$total = preg_match_all('/#(\w+)/u', $tuit, $coincidencias);
echo "Hashtags encontrados: $total" . PHP_EOL; // 3
print_r($coincidencias[1]); // [PHP, programación, 2026]
preg_replace() y preg_replace_callback(): reemplazar
<?php
declare(strict_types=1);
// Normalizar espacios: cualquier grupo de espacios, tabuladores o saltos → un espacio
$sucio = "Texto con espacios\n\nraros";
echo preg_replace('/\s+/', ' ', $sucio) . PHP_EOL; // Texto con espacios raros
// Ocultar los dígitos de una tarjeta salvo los 4 últimos
$tarjeta = '4111 1111 1111 1234';
echo preg_replace('/\d(?=(?:\s?\d){4})/', '*', $tarjeta) . PHP_EOL; // **** **** **** 1234
// Reordenar con referencias a grupos: dd/mm/aaaa → aaaa-mm-dd
echo preg_replace('#(\d{2})/(\d{2})/(\d{4})#', '$3-$2-$1', '03/10/2026') . PHP_EOL; // 2026-10-03
// Reemplazo calculado con una función
echo preg_replace_callback(
'/\d+/',
fn(array $m): string => (string) ((int) $m[0] * 2),
'Tengo 3 gatos y 12 peces'
) . PHP_EOL;
// Tengo 6 gatos y 24 peces
preg_split(): dividir con un patrón
<?php
declare(strict_types=1);
// Separar por comas, puntos y comas o espacios (en cualquier cantidad)
$entrada = 'manzana, pera;plátano uva';
$frutas = preg_split('/[\s,;]+/', $entrada, -1, PREG_SPLIT_NO_EMPTY);
print_r($frutas); // [manzana, pera, plátano, uva]
Validaciones habituales
Correo electrónico
<?php
declare(strict_types=1);
// ✅ Lo recomendado: el filtro nativo de PHP, mantenido y probado
function emailValido(string $email): bool
{
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}
var_dump(emailValido('ana@ejemplo.es')); // bool(true)
var_dump(emailValido('ana@@ejemplo')); // bool(false)
// Una regex razonable (más permisiva y simple; útil para entender el patrón)
$patron = '/^[\w.+-]+@[\w-]+(\.[\w-]+)*\.[a-z]{2,}$/i';
var_dump(preg_match($patron, 'ana.lopez+web@ejemplo.com.es')); // int(1)
var_dump(preg_match($patron, 'ana@ejemplo')); // int(0)
💡 Consejo: La especificación real de las direcciones de correo es enormemente compleja. Para validar emails usa
filter_var()y, si es importante, confírmalo enviando un correo con un enlace de verificación.
Código postal español
Cinco dígitos, y los dos primeros corresponden a una provincia (01 a 52):
<?php
declare(strict_types=1);
function codigoPostalValido(string $cp): bool
{
return preg_match('/^(?:0[1-9]|[1-4]\d|5[0-2])\d{3}$/', $cp) === 1;
}
foreach (['28013', '08001', '52001', '53000', '00123', '2801'] as $cp) {
echo $cp . ': ' . (codigoPostalValido($cp) ? 'válido' : 'no válido') . PHP_EOL;
}
// 28013: válido
// 08001: válido
// 52001: válido
// 53000: no válido
// 00123: no válido
// 2801: no válido
DNI español
Ocho dígitos y una letra de control. La letra se obtiene con el resto de dividir el número entre 23, buscando en la secuencia TRWAGMYFPDXBNJZSQVHLCKE:
<?php
declare(strict_types=1);
function dniValido(string $dni): bool
{
$dni = strtoupper(trim($dni));
// 1. Formato: 8 dígitos + 1 letra
if (preg_match('/^(?<numero>\d{8})(?<letra>[A-Z])$/', $dni, $partes) !== 1) {
return false;
}
// 2. Letra de control
$letras = 'TRWAGMYFPDXBNJZSQVHLCKE';
$letraCorrecta = $letras[(int) $partes['numero'] % 23];
return $letraCorrecta === $partes['letra'];
}
var_dump(dniValido('12345678Z')); // bool(true)
var_dump(dniValido('12345678A')); // bool(false) - la letra no corresponde
var_dump(dniValido('1234567Z')); // bool(false) - faltan dígitos
💡 Consejo: Las expresiones regulares son potentísimas pero difíciles de leer. Si una función nativa resuelve el problema (
str_contains(),ctype_digit(),filter_var()), úsala. Y cuando escribas una regex compleja, acompáñala de un comentario y de pruebas con casos válidos e inválidos. Herramientas online como regex101.com (eligiendo el sabor PCRE) ayudan mucho a depurarlas.
11. htmlspecialchars(): escapar la salida (adelanto de seguridad)
Cuando muestras en una página un texto que viene del usuario (un comentario, su nombre, un parámetro de la URL), ese texto podría contener HTML o JavaScript malicioso:
<?php
// Imagina que esto lo ha escrito un usuario en un formulario de comentarios
$comentario = '<script>alert("Te he robado la sesión")</script>';
// ❌ PELIGRO: el navegador ejecutará el script (ataque XSS)
echo "<p>$comentario</p>";
// ✅ Escapado: el navegador muestra el texto, no lo ejecuta
echo '<p>' . htmlspecialchars($comentario) . '</p>';
// <p><script>alert("Te he robado la sesión")</script></p>
| Carácter | Se convierte en |
|---|---|
& | & |
< | < |
> | > |
" | " |
' | ' |
⚠️ Importante: Regla de oro: escapa todo lo que imprimas en HTML si no lo has escrito tú. Una función auxiliar corta ayuda a no olvidarlo:
<?php
declare(strict_types=1);
function e(string $texto): string
{
return htmlspecialchars($texto, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
$busqueda = $_GET['q'] ?? '';
?>
<p>Resultados para: <strong><?= e($busqueda) ?></strong></p>
<input type="search" name="q" value="<?= e($busqueda) ?>">
📘 Concepto: Los flags
ENT_QUOTES | ENT_SUBSTITUTEy la codificación UTF-8 son ya los valores por defecto desde PHP 8.1, pero escribirlos explícitamente deja clara la intención. Profundizarás en XSS, inyección SQL y otros ataques en el módulo 16.
12. Fechas como cadenas
Las fechas aparecen constantemente como texto: en la interfaz, en ficheros, en la base de datos. Aquí verás lo básico; el manejo avanzado (zonas horarias, intervalos, periodos) lo irás viendo a lo largo del curso.
date(): formatear la fecha actual
<?php
declare(strict_types=1);
echo date('d/m/Y') . PHP_EOL; // 03/10/2026
echo date('H:i:s') . PHP_EOL; // 17:45:12
echo date('Y-m-d H:i:s') . PHP_EOL; // 2026-10-03 17:45:12 (formato de bases de datos)
echo date('d/m/Y', 0) . PHP_EOL; // 01/01/1970 (fecha de un timestamp concreto)
| Carácter | Significado | Ejemplo |
|---|---|---|
d / j | Día con / sin cero inicial | 03 / 3 |
m / n | Mes con / sin cero inicial | 10 / 10 |
Y / y | Año con 4 / 2 dígitos | 2026 / 26 |
H / G | Hora 24h con / sin cero | 09 / 9 |
i | Minutos | 05 |
s | Segundos | 07 |
N | Día de la semana (1 = lunes, 7 = domingo) | 6 |
D / l | Día de la semana en inglés (corto / largo) | Sat / Saturday |
t | Días que tiene el mes | 31 |
L | ¿Año bisiesto? (1/0) | 0 |
⚠️ Importante:
date()usa la zona horaria configurada enphp.ini(date.timezone, módulo 01). Si no la configuraste, trabajará en UTC y verás horas desfasadas.
DateTimeImmutable: la forma moderna
DateTimeImmutable representa una fecha como objeto. Es inmutable: cada modificación devuelve un objeto nuevo sin alterar el original, lo que evita errores muy difíciles de detectar:
<?php
declare(strict_types=1);
$hoy = new DateTimeImmutable('2026-10-03 17:45');
echo $hoy->format('d/m/Y H:i') . PHP_EOL; // 03/10/2026 17:45
$enUnMes = $hoy->modify('+1 month');
echo $enUnMes->format('d/m/Y') . PHP_EOL; // 03/11/2026
echo $hoy->format('d/m/Y') . PHP_EOL; // 03/10/2026 ← el original no cambia
// Diferencia entre dos fechas
$navidad = new DateTimeImmutable('2026-12-25');
$diferencia = $hoy->diff($navidad);
echo "Faltan {$diferencia->days} días para Navidad" . PHP_EOL; // Faltan 82 días para Navidad
// Leer una fecha escrita en formato español
$fecha = DateTimeImmutable::createFromFormat('d/m/Y', '15/08/2026');
echo $fecha->format('Y-m-d') . PHP_EOL; // 2026-08-15
Nombres de meses y días en español
format() escribe los nombres en inglés. La forma más sencilla de mostrarlos en español es un array de traducción (para aplicaciones multi-idioma, la clase IntlDateFormatter de la extensión intl):
<?php
declare(strict_types=1);
function fechaLarga(DateTimeImmutable $fecha): string
{
$dias = ['lunes', 'martes', 'miércoles', 'jueves', 'viernes', 'sábado', 'domingo'];
$meses = ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio',
'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'];
$diaSemana = $dias[(int) $fecha->format('N') - 1];
$mes = $meses[(int) $fecha->format('n') - 1];
return sprintf('%s, %d de %s de %s', $diaSemana, (int) $fecha->format('j'), $mes, $fecha->format('Y'));
}
echo mb_ucfirst(fechaLarga(new DateTimeImmutable('2026-10-03'))) . PHP_EOL;
// Sábado, 3 de octubre de 2026
💡 Consejo: Usa
DateTimeImmutableen lugar deDateTime(la versión mutable). ConDateTime,$fin = $inicio->modify('+1 day')modifica también$inicio, un error clásico.
13. Ejercicios
Ejercicio 1: Generador de slugs
Escribe una función slug(string $titulo): string que convierta un título en una URL amigable: '¡Las 10 Mejores Recetas de Otoño!' → 'las-10-mejores-recetas-de-otono'. Pasos: minúsculas con mb_strtolower(), sustituir vocales con tilde, ñ y ü con strtr(), sustituir con preg_replace() todo lo que no sea letra o número por guiones, eliminar guiones repetidos y quitar los de los extremos con trim().
Ejercicio 2: Análisis de un texto
Dado un párrafo de al menos 50 palabras, muestra: número de caracteres (con mb_strlen()), número de palabras, número de frases (cuenta los ., ! y ? con preg_match_all()), la palabra más larga, y el texto con la primera letra de cada frase en mayúscula. Compara strlen() y mb_strlen() y explica la diferencia.
Ejercicio 3: Plantilla de correo con heredoc
Crea con heredoc un correo HTML de confirmación de pedido que incluya el nombre del cliente, el número de pedido, una tabla con las líneas (genera las filas con un foreach y .= antes del heredoc) y el total formateado con number_format() al estilo español. Escapa con htmlspecialchars() el nombre del cliente.
Ejercicio 4: Validador de formulario
Escribe funciones de validación que devuelvan true o false para: un DNI, un NIE (empieza por X, Y o Z, que se sustituyen por 0, 1 o 2 antes de calcular la letra), un código postal español, un teléfono móvil español (9 dígitos que empiezan por 6 o 7, admitiendo opcionalmente el prefijo +34 y espacios) y un email. Prueba cada una con al menos tres casos válidos y tres inválidos.
Ejercicio 5: Ticket de compra alineado
Genera en la terminal un ticket de supermercado con printf(): cabecera con el nombre de la tienda centrado con mb_str_pad(..., STR_PAD_BOTH), fecha y hora con date(), una línea por producto (nombre a la izquierda, unidades, precio unitario y subtotal a la derecha), base imponible, IVA y total. Incluye productos con tildes y comprueba que todo queda alineado.
Ejercicio 6: Extractor de datos
Dado este texto, extrae con expresiones regulares todos los emails, todas las fechas en formato dd/mm/aaaa (convirtiéndolas a aaaa-mm-dd con grupos con nombre) y todos los importes en euros (números seguidos de €, con o sin decimales):
Contactar con ana@empresa.es antes del 15/11/2026. El presupuesto es de 1.250,50 €
y la segunda fase costará 800 €. Copia a soporte@empresa.es. Revisión el 01/12/2026.
Ejercicio 7: Corrector ortográfico básico
Crea un “diccionario” con 20 palabras. Pide al usuario una palabra con readline() y, si no está en el diccionario, sugiere las tres más parecidas según levenshtein(), ordenadas de más a menos parecida. Normaliza la entrada con mb_trim() y mb_strtolower().
Resumen
| Concepto | Sintaxis / función |
|---|---|
| Comillas simples | 'literal $sin interpolar' |
| Comillas dobles | "Hola $nombre", "{$datos['clave']}" |
| Escapes | \n, \t, \\, \$, \", \u{1F418} |
| Heredoc / nowdoc | <<<TEXTO ... TEXTO; / <<<'TEXTO' ... TEXTO; |
| Concatenar | $a . $b, $a .= $b, implode() |
| Buscar | str_contains(), str_starts_with(), str_ends_with(), strpos() !== false |
| Extraer | substr(), mb_substr() |
| Reemplazar | str_replace(), strtr(), preg_replace() |
| Limpiar | trim(), mb_trim() (8.4), ltrim(), rtrim() |
| Mayúsculas | mb_strtoupper(), mb_strtolower(), mb_ucfirst() (8.4) |
| Rellenar | str_pad(), mb_str_pad() (8.3), str_repeat() |
| Dividir / unir | explode(), implode(), preg_split() |
| Longitud | strlen() (bytes), mb_strlen() (caracteres) |
| Formato | printf(), sprintf('%05.2f', $n), number_format($n, 2, ',', '.') |
| Comparar | ===, strcmp(), strcasecmp(), levenshtein(), similar_text() |
| Regex | preg_match(), preg_match_all(), preg_replace(), preg_split() |
| Grupos con nombre | (?<nombre>...) → $coincidencias['nombre'] |
| Escapar HTML | htmlspecialchars($s, ENT_QUOTES \| ENT_SUBSTITUTE, 'UTF-8') |
| Fechas | date('d/m/Y'), new DateTimeImmutable(), ->format(), ->modify() |