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() y number_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() y DateTimeImmutable

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 usa sprintf() (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() o str_ends_with(). Reserva strpos() 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 versiones mb_*.

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 con utf8mb4 (módulo 15) y funciones mb_* cuando el texto pueda contener caracteres no ASCII. El valor por defecto de mbstring en 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 $s como 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') produce 1.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() y similar_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>&lt;script&gt;alert(&quot;Te he robado la sesión&quot;)&lt;/script&gt;</p>
Carácter Se convierte en
& &amp;
< &lt;
> &gt;
" &quot;
' &#039;

⚠️ 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_SUBSTITUTE y 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 en php.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 DateTimeImmutable en lugar de DateTime (la versión mutable). Con DateTime, $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()