Fundamentos
Variables, Tipos y Constantes
Las variables de PHP comienzan con $ y son de tipado dinámico. PHP 7.4+ soporta propiedades tipadas. Use define() para constantes en tiempo de ejecución y const para constantes en tiempo de compilación (más rápido). PHP 8.1 introdujo enums — un sistema de enumeración tipada superior a las constantes de clase. Siempre declare tipos cuando sea posible (PHP 7+) para detección temprana de errores y mejor soporte de IDE.
<?php
$name = "Alice"; // string
$age = 30; // integer
$price = 19.99; // float (double)
$active = true; // boolean
$items = [1, 2, 3]; // array
$null = null; // null
// constants
define("MAX_USERS", 100); // runtime constant
const PI = 3.14159; // compile-time constant
echo MAX_USERS; // no $ prefix
// PHP 8.1+ enums
enum Status: string {
case Active = 'active';
case Inactive = 'inactive';
}
$s = Status::Active;Echo, Print y Depuración
echo es un constructo del lenguaje (no una función) — el más rápido para salida. print devuelve 1 así que puede usarse en expresiones. printf/sprintf usan especificadores de formato estilo C (%s cadena, %d entero, %f float, %x hex). var_dump() es la herramienta principal de depuración — muestra tipos y valores. error_log() escribe al log de errores de PHP o syslog. En producción, nunca exponga salida de depuración a los usuarios.
<?php
// echo: no return, multiple args (fastest)
echo "Hello", " ", "World";
// print: returns 1, single arg
print "Hello World";
// printf: formatted output
printf("Name: %s, Age: %d, Price: %.2f", "Alice", 30, 19.99);
// debugging output
print_r($array); // human-readable
var_dump($variable); // type + value (detailed)
var_export($array, true); // valid PHP code (for caching)
// sprintf: return formatted string (don't print)
$log = sprintf("[%s] %s", date('H:i:s'), "Started");
error_log($log); // log to error logOperadores y Comparaciones
Siempre use === (comparación estricta) para evitar bugs de coerción de tipos. == convierte tipos antes de comparar, llevando a resultados sorprendentes (0 == 'abc' era verdadero en PHP 7). El operador spaceship (<=>) devuelve -1/0/1 — útil para usort. El operador null coalescing (??) es la forma idiomática de proporcionar defaults. El operador null-safe (?->) (PHP 8+) cortocircuita cadenas de métodos en null, reemplazando verbosas verificaciones isset().
<?php
// arithmetic
$sum = 10 + 3; // 13
$mod = 10 % 3; // 1
$pow = 2 ** 3; // 8 (PHP 5.6+)
// comparison: == vs ===
echo (0 == "abc"); // true (loose, PHP 7-); false (PHP 8+)
echo (0 === "abc"); // false (strict — type + value)
echo ("1" == 1); // true (loose)
echo ("1" === 1); // false (strict)
// spaceship operator (PHP 7+)
echo 1 <=> 2; // -1 (less than)
echo 2 <=> 2; // 0 (equal)
echo 3 <=> 2; // 1 (greater than)
// null coalescing
$name = $input ?? "default"; // if $input is null
$deep = $data['user']['name'] ?? "Anonymous";
// null safe operator (PHP 8+)
$country = $user?->getAddress()?->country; // null if any step is nullSuperglobales y Web
Las superglobales son arrays asociativos integrados disponibles en todos los scopes. $_GET y $_POST contienen entrada de usuario — SIEMPRE sanitize/valide antes de usar. filter_input() es más seguro que el acceso directo. Nunca confíe en valores de $_SERVER que pueden ser falsificados por clientes (como HTTP_USER_AGENT). Siempre llame exit después de header('Location:') — PHP continúa ejecutándose de lo contrario. Inicie sesiones con session_start() antes de cualquier salida.
<?php
// superglobals available everywhere
$_GET['name']; // query string params
$_POST['email']; // form POST data
$_REQUEST['x']; // GET + POST + COOKIE
$_SERVER['HTTP_HOST']; // server/env info
$_SERVER['REQUEST_METHOD']; // GET, POST, etc.
$_COOKIE['session']; // cookies
$_FILES['upload']; // file uploads
$_SESSION['user_id']; // session data (after session_start())
// get client IP
$ip = $_SERVER['REMOTE_ADDR'];
// check request method
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$name = filter_input(INPUT_POST, 'name', FILTER_SANITIZE_STRING);
}
// redirect
header("Location: /dashboard");
exit; // always exit after redirectInclude y Require
include/require ejecutan el archivo especificado. require causa un error fatal si el archivo falta (use para dependencias críticas); include solo advierte (use para plantillas opcionales). Las variantes _once rastrean archivos incluidos para prevenir doble inclusión — esencial para definiciones de funciones/clases. El autoloader de Composer (require_once 'vendor/autoload.php') elimina la gestión manual de includes. Los archivos pueden devolver valores, haciéndolos útiles para configuración.
<?php
// include: warning on failure, continues
include 'header.php';
include_once 'config.php'; // only once
// require: fatal error on failure, stops
require 'database.php';
require_once 'vendor/autoload.php'; // Composer autoload
// _once variants prevent re-declaration errors
// use require for critical files (DB config, autoloaders)
// use include for optional files (templates)
// return values from included files
$config = include 'config.php';
// config.php: return ['db' => 'mysql://...', 'debug' => true];Cadenas
Funciones de Cadena
PHP tiene más de 100 funciones de cadena. strpos() devuelve false si no se encuentra — use === false para verificar (0 es una posición válida). str_replace() puede tomar arrays para buscar/reemplazar. substr() soporta offsets negativos (desde el final). Para cadenas multibyte (UTF-8), use equivalentes mb_* (mb_strlen, mb_substr) — strlen cuenta bytes, no caracteres. Siempre establezca default_charset='UTF-8' y use funciones mb_* para texto no-ASCII.
<?php
$s = "Hello, World";
// length and case
echo strlen($s); // 12
echo str_word_count($s); // 2
echo strtoupper($s); // HELLO, WORLD
echo strtolower($s); // hello, world
echo ucfirst("hello"); // Hello
echo ucwords("hello world"); // Hello World
// search and replace
echo strpos($s, "World"); // 7 (false if not found)
echo str_replace("o", "0", $s); // Hell0, W0rld
echo substr($s, 0, 5); // Hello
echo substr($s, -5); // World
echo strrev($s); // dlroW ,olleH
// trim
echo trim(" hi "); // "hi"
echo ltrim(" hi"); // "hi"
echo rtrim("hi "); // "hi"Interpolación de Cadenas y Heredoc
Las cadenas con comillas dobles interpolan variables; las de comillas simples no (use comillas simples para texto literal — ligeramente más rápido). Use {$var} para expresiones complejas (propiedades de objeto, acceso a array, llamadas a métodos). Heredoc (<<<ID) es ideal para cadenas multilínea como SQL o HTML — interpola variables. Nowdoc (<<<'ID') es la versión no interpretante, útil para patrones regex con backslashes.
<?php
$name = "Alice";
$age = 30;
// double quotes: variable interpolation
echo "Hello, $name!"; // Hello, Alice!
echo "Hello, {$name}!"; // Hello, Alice! (braces for clarity)
echo "Age: {$age}"; // Age: 30
// single quotes: no interpolation (faster)
echo 'Hello, $name'; // Hello, $name (literal)
// complex expressions need braces
echo "Result: {$obj->method()}";
echo "Item: {$array['key']}";
// heredoc: multi-line, interpolates
$sql = <<<SQL
SELECT * FROM users
WHERE name = '$name'
AND age > $age
SQL;
// nowdoc: multi-line, NO interpolation (like single quotes)
$regex = <<<'REGEX'
\d{3}-\d{4}
REGEX;sprintf y Formato
sprintf() es esencial para construir cadenas formateadas de forma segura — a diferencia de la interpolación de cadenas, maneja conversiones de tipo y padding. Use %d para enteros (no %s) para asegurar formato numérico. number_format() formatea números con separadores de miles — crítico para visualización de moneda. Siempre use sprintf para fragmentos SQL en sentencias preparadas (aunque las sentencias preparadas siguen siendo necesarias para entrada de usuario).
<?php
// sprintf: format string (returns, doesn't print)
$formatted = sprintf(
"%-10s | %5d | %8.2f",
"Alice", 42, 19.99
);
echo $formatted; // "Alice | 42 | 19.99"
// format specifiers:
// %s string, %d int, %f float, %x hex, %b binary, %c char
// %5d right-padded to width 5
// %-5d left-padded
// %05d zero-padded
// %.2f 2 decimal places
// %8.2f width 8, 2 decimals
// numbered placeholders
echo sprintf("Hi %1$s, bye %1$s", "Alice"); // Hi Alice, bye Alice
// number_format
echo number_format(1234567.891, 2); // 1,234,567.89
echo number_format(1234567.89, 2, ',', '.'); // European formatRegex (PCRE)
PHP usa PCRE (Expresiones Regulares Compatibles con Perl) con delimitadores /pattern/. preg_match devuelve 1 si coincide, 0 si no (use ===, no ==, ya que 0 es falsy). Siempre valide entrada de usuario con regex pero no dependa solo de ello — use filter_var() para emails, URLs. preg_replace es potente pero puede ser lento en cadenas grandes. Use [^...] para whitelist caracteres en lugar de blacklist. El flag i hace la coincidencia insensible a mayúsculas/minúsculas.
<?php
// preg_match: test pattern (returns 0 or 1)
if (preg_match('/^[a-z]+$/i', "Hello")) {
echo "alphabetic";
}
// capture groups
preg_match('/(d{4})-(d{2})-(d{2})/', "2024-01-15", $matches);
// $matches[0] = "2024-01-15" (full match)
// $matches[1] = "2024" (group 1)
// $matches[2] = "01"
// $matches[3] = "15"
// preg_match_all: all matches
preg_match_all('/d+/', "a1 b22 c333", $matches);
// $matches[0] = ["1", "22", "333"]
// preg_replace: substitute
$clean = preg_replace('/[^a-z0-9]/i', '', "Hello, World!"); // HelloWorld
// preg_split: split by pattern
$parts = preg_split('/[s,]+/', "one, two,three four");
// ["one", "two", "three", "four"]Multibyte y Codificación
Las funciones de cadena predeterminadas de PHP están orientadas a bytes, no a caracteres — se rompen en caracteres multibyte (UTF-8, chino, emoji). Siempre use funciones mb_* (mb_strlen, mb_substr, mb_strpos, mb_strtoupper) para texto no-ASCII. Establezca mb_internal_encoding('UTF-8') al inicio de su aplicación. Use JSON_UNESCAPED_UNICODE para mantener chino/emoji legible en salida JSON. Esta es una fuente común de bugs en aplicaciones internacionales.
<?php
// UTF-8 strings: use mb_* functions
$text = "Café"; // 4 chars, 5 bytes (é = 2 bytes)
echo strlen($text); // 5 (bytes!)
echo mb_strlen($text); // 4 (characters!)
echo substr($text, 0, 3); // "Caf" (might break UTF-8!)
echo mb_substr($text, 0, 3); // "Caf" (safe)
// case conversion
echo strtoupper("straße"); // "STRAßE" (wrong!)
echo mb_strtoupper("straße"); // "STRAßE" (correct, locale-aware)
// encoding detection
$encoding = mb_detect_encoding($text);
$utf8 = mb_convert_encoding($text, 'UTF-8', 'auto');
// set internal encoding
mb_internal_encoding('UTF-8');
mb_regex_encoding('UTF-8');
// JSON with UTF-8
$json = json_encode($data, JSON_UNESCAPED_UNICODE);Arrays
Arrays Indexados y Asociativos
Los arrays de PHP son en realidad hash maps ordenados — funcionan tanto como listas como diccionarios. Los arrays indexados auto-asignan claves numéricas; los arrays asociativos usan claves de cadena. isset() devuelve false para valores null; array_key_exists() devuelve true incluso para null. unset() elimina un elemento pero no reindexa. Para una lista verdadera (sin huecos), use array_values() para reindexar después de eliminar. PHP 8.1+ tiene un tipo array readonly.
<?php
// indexed array (numeric keys)
$nums = [10, 20, 30];
$nums[] = 40; // append
echo $nums[0]; // 10
echo count($nums); // 4
// associative array (string keys)
$user = [
"name" => "Alice",
"age" => 30,
"email" => "[email protected]",
];
echo $user["name"]; // Alice
$user["phone"] = "555-1234"; // add key
// mixed keys
$mixed = [0 => "a", "name" => "b", 5 => "c"];
// check key existence
if (isset($user["email"])) { /* ... */ }
if (array_key_exists("name", $user)) { /* ... */ }
// remove element
unset($user["phone"]);Multidimensionales e Iteración
Los arrays multidimensionales son arrays de arrays. foreach es la forma idiomática de iterar — es más rápido y legible que los bucles for. Use &$value para modificar elementos in place (siempre haga unset de la referencia después del bucle para evitar bugs). array_column() extrae una única columna de un array 2D — extremadamente útil para transformar conjuntos de resultados de base de datos. Los arrays de PHP mantienen el orden de inserción.
<?php
$users = [
["name" => "Alice", "age" => 30],
["name" => "Bob", "age" => 25],
["name" => "Carol", "age" => 35],
];
// iterate with key + value
foreach ($users as $index => $user) {
echo "$index: {$user['name']} ({$user['age']})\n";
}
// modify by reference
foreach ($users as &$user) {
$user['age'] += 1; // increment each age
}
unset($user); // break reference!
// nested iteration
$matrix = [[1, 2], [3, 4], [5, 6]];
foreach ($matrix as $row) {
foreach ($row as $cell) {
echo $cell . " ";
}
echo "\n";
}
// extract column
$names = array_column($users, 'name'); // ["Alice", "Bob", "Carol"]Funciones de Array: map, filter, reduce
array_map, array_filter y array_reduce son el trío de programación funcional para arrays. Las arrow functions (fn() =>) los hacen concisos. array_filter preserva claves — use array_values() para reindexar si es necesario. array_merge reindexa claves numéricas pero preserva claves de cadena (valores posteriores sobrescriben). array_column, array_chunk y array_slice son esenciales para manipulación de datos. Estas funciones son la columna vertebral del procesamiento de datos en PHP.
<?php
$nums = [1, 2, 3, 4, 5];
// map: transform each element
$doubled = array_map(fn($n) => $n * 2, $nums); // [2, 4, 6, 8, 10]
// filter: keep elements matching condition
$evens = array_filter($nums, fn($n) => $n % 2 === 0); // [2, 4]
// reduce: accumulate to single value
$sum = array_reduce($nums, fn($carry, $n) => $carry + $n, 0); // 15
// walk: like map but modifies in place (by reference)
array_walk($nums, fn(&$n) => $n *= 2);
// combining arrays
$merged = array_merge([1, 2], [3, 4]); // [1, 2, 3, 4]
$combined = array_combine(['a', 'b'], [1, 2]); // ['a' => 1, 'b' => 2]
$sliced = array_slice($nums, 1, 2); // [2, 3]
$chunked = array_chunk($nums, 2); // [[1,2], [3,4], [5]]Ordenar Arrays
Las funciones de ordenación de PHP modifican el array in place (pasaje por referencia). sort/rsort reindexan; asort/arsort preservan claves. usort con una función de comparación (usando <=>) ordena por lógica personalizada. natsort() hace ordenación natural (img2 antes de img10) — esencial para nombres de archivo. Para arrays multidimensionales, use usort con un closure que compare el campo deseado. El operador spaceship (<=>) simplifica las funciones de comparación.
<?php
$nums = [3, 1, 4, 1, 5, 9, 2, 6];
// sort by value (reindexes)
sort($nums); // [1, 1, 2, 3, 4, 5, 6, 9]
rsort($nums); // descending
// sort preserving keys
asort($nums); // ascending, preserve keys
arsort($nums); // descending, preserve keys
// sort by key
ksort($nums); // by key ascending
krsort($nums); // by key descending
// custom sort with callback
$users = [["name" => "Bob", "age" => 25], ["name" => "Alice", "age" => 30]];
usort($users, fn($a, $b) => $a['age'] <=> $b['age']);
// sorted by age ascending
// natural sort (for strings with numbers)
$files = ["img10.jpg", "img2.jpg", "img1.jpg"];
natsort($files); // ["img1.jpg", "img2.jpg", "img10.jpg"]
sort($files); // ["img1.jpg", "img10.jpg", "img2.jpg"] (wrong!)Inspección y Manipulación de Arrays
in_array con strict=true (tercer parámetro) previene bugs de coerción de tipos. array_search devuelve la clave (use === false para verificar). array_push/pop implementan LIFO (pila); array_shift/unshift implementan FIFO (cola) — pero shift es O(n). Para colas grandes, use SplQueue o SplDoublyLinkedList. array_unique preserva claves. array_diff/intersect comparan valores; use array_diff_key/intersect_key para comparación basada en claves.
<?php
$arr = [1, 2, 3, 4, 5];
// inspection
echo count($arr); // 5
echo in_array(3, $arr); // true
echo in_array("3", $arr, true); // false (strict)
echo array_search(3, $arr); // 2 (key, false if not found)
print_r(array_keys($arr)); // [0, 1, 2, 3, 4]
print_r(array_values($arr)); // [1, 2, 3, 4, 5]
// stack/queue operations
array_push($arr, 6); // push to end
$last = array_pop($arr); // pop from end
$first = array_shift($arr); // remove from front
array_unshift($arr, 0); // add to front
// set operations
$unique = array_unique([1, 2, 2, 3]); // [1, 2, 3]
$diff = array_diff([1, 2, 3], [2, 3, 4]); // [1] (in first, not second)
$intersect = array_intersect([1, 2, 3], [2, 3, 4]); // [2, 3]
// flip and reverse
$flipped = array_flip(['a' => 1, 'b' => 2]); // [1 => 'a', 2 => 'b']
$reversed = array_reverse([1, 2, 3]); // [3, 2, 1]Flujo de Control
If / Else / Elseif
PHP usa elseif (una palabra) — no 'else if' con espacio (aunque eso también funciona). La sintaxis alternativa (if: ... endif;) es útil en plantillas HTML para evitar confusión de llaves. El operador ternario es asociativo por la derecha — evite anidar. El operador de asignación null coalescing (??=) establece un valor solo si actualmente es null — perfecto para inicialización perezosa de defaults de configuración.
<?php
$score = 85;
if ($score >= 90) {
$grade = "A";
} elseif ($score >= 80) {
$grade = "B";
} elseif ($score >= 70) {
$grade = "C";
} else {
$grade = "F";
}
// alternative syntax (for templates)
if ($score >= 90):
echo "Excellent";
elseif ($score >= 80):
echo "Good";
else:
echo "Try harder";
endif;
// ternary
$status = $age >= 18 ? "adult" : "minor";
// null coalescing assignment (PHP 7.4+)
$config['timeout'] ??= 30; // set if not setSwitch y Match
switch usa comparación floja (==) y requiere break para prevenir caída — una fuente común de bugs. match (PHP 8+) usa comparación estricta (===), devuelve un valor directamente y lanza una excepción si ningún brazo coincide (sin fallos silenciosos). match es el reemplazo moderno de switch cuando necesita un valor. Use switch para casos complejos multi-sentencia; use match para selección simple de valor. Siempre incluya un caso default.
<?php
// switch: loose comparison (==)
$day = "Mon";
switch ($day) {
case "Mon":
case "Tue":
case "Wed":
echo "Weekday";
break;
case "Sat":
case "Sun":
echo "Weekend";
break;
default:
echo "Unknown";
}
// match (PHP 8+): strict comparison (===), returns value
$status = 404;
$message = match($status) {
200, 201 => "Success",
301, 302 => "Redirect",
404 => "Not Found",
500 => "Server Error",
default => "Unknown",
};
// match throws UnhandledMatchError if no match and no defaultBucles: for, while, foreach, do-while
foreach es el bucle idiomático para arrays — es más rápido y seguro que for con count(). Use continue para saltar iteraciones y break para salir. PHP no tiene break/continue etiquetado (a diferencia de Java/Rust). Para arrays asociativos, foreach ($arr as $key => $value) es el patrón estándar. do-while se ejecuta al menos una vez — útil para validación de entrada. Evite modificar el array durante foreach (use un array separado para resultados).
<?php
// for loop
for ($i = 0; $i < 5; $i++) {
echo $i; // 01234
}
// foreach (most common in PHP)
$fruits = ["apple", "banana", "cherry"];
foreach ($fruits as $fruit) {
echo $fruit;
}
// foreach with key
foreach ($fruits as $index => $fruit) {
echo "$index: $fruit";
}
// while
$count = 0;
while ($count < 3) {
echo $count++;
}
// do-while (runs at least once)
do {
$line = readline("> ");
} while ($line !== "quit");
// break and continue
for ($i = 0; $i < 10; $i++) {
if ($i === 3) continue; // skip 3
if ($i === 7) break; // stop at 7
echo $i;
}Flujo de Control en Plantillas
La sintaxis de control alternativa de PHP (if:/elseif:/else:/endif;, foreach:/endforeach;) está diseñada para plantillas HTML. <?= $var ?> es abreviatura de <?php echo $var; ?> — úselo siempre en plantillas por legibilidad. Siempre escape la salida con htmlspecialchars() para prevenir XSS. La separación de lógica PHP y presentación HTML es la base de sistemas de plantillas como Twig y Blade, que ofrecen sintaxis más limpia y escape automático.
<?php // template file ?>
<?php if ($user->isAdmin()): ?>
<div class="admin-panel">Admin Tools</div>
<?php elseif ($user->isEditor()): ?>
<div class="editor-tools">Edit Tools</div>
<?php else: ?>
<div class="user-view">Read Only</div>
<?php endif; ?>
<?php foreach ($products as $p): ?>
<div class="product">
<?= htmlspecialchars($p['name']) ?>
- $<?= number_format($p['price'], 2) ?>
</div>
<?php endforeach; ?>
<?php // shorthand echo ?>
<h1><?= $title ?></h1>
<?php // ternary in templates ?>
<span class="<?= $active ? 'on' : 'off' ?>"><?= $active ? 'Active' : 'Inactive' ?></span>Excepciones y Manejo de Errores
PHP 7+ usa excepciones para la mayoría de errores. Siempre capture tipos de excepción específicos (no solo Exception) para manejar diferentes fallos apropiadamente. finally siempre se ejecuta — úselo para limpieza (cerrar archivos, conexiones). Las excepciones personalizadas extienden Exception y añaden contexto de dominio. PHP 8+ permite capturar múltiples tipos de excepción con |. Establezca PDO a modo excepción para manejo de errores consistente. Nunca capture excepciones sin loguear — los fallos silenciosos ocultan bugs.
<?php
// try / catch / finally
try {
$pdo = new PDO("mysql:host=localhost;dbname=test", "user", "pass");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
error_log("DB connection failed: " . $e->getMessage());
die("Service unavailable");
} finally {
// always runs, even after return/throw
echo "Cleanup";
}
// custom exception
class ValidationException extends Exception {
public function __construct(string $field, string $message = "") {
parent::__construct("$field: $message");
}
}
// throw
if (empty($email)) {
throw new ValidationException("email", "is required");
}
// catch multiple types (PHP 8+)
try {
riskyOperation();
} catch (PDOException | RuntimeException $e) {
// catches either type
log_error($e);
}Funciones
Definir Funciones
PHP 7+ soporta parámetros tipados y tipos de retorno (int, string, array, ?Type para nullable). PHP 8+ añade argumentos nombrados (omitir defaults, reordenar params), union types (int|string) y tipo mixed. Parámetros variadic (...$nums) recogen argumentos extra en un array. El operador spread (...$arr) desempaqueta un array como argumentos. Pasaje por referencia (&) modifica el original — úselo con moderación ya que hace el código más difícil de razonar.
<?php
// basic function with return type
function add(int $a, int $b): int {
return $a + $b;
}
// default parameters
function greet(string $name, string $greeting = "Hello"): string {
return "$greeting, $name!";
}
// named arguments (PHP 8+)
echo greet(name: "Alice", greeting: "Hi");
// variadic functions
function sum(int ...$nums): int {
return array_sum($nums);
}
echo sum(1, 2, 3, 4); // 10
// spread operator
$nums = [1, 2, 3];
echo sum(...$nums); // 6
// pass by reference
function increment(int &$n): void {
$n++;
}
$x = 5;
increment($x);
echo $x; // 6Arrow Functions y Closures
Las arrow functions (fn() =>) son closures concisos de una sola expresión que capturan automáticamente variables externas por valor. Los closures tradicionales (function() use ($var)) se necesitan para cuerpos multilínea o captura por referencia (&$var). Los closures son esenciales para array_map, array_filter, usort y manejadores de eventos. Las arrow functions no pueden tener sentencias (no if, for) — use closures tradicionales para lógica compleja. Los closures son objetos de primera clase (clase Closure).
<?php
// arrow function (PHP 7.4+): single expression, auto-capture
$square = fn($x) => $x * $x;
echo $square(5); // 25
// auto-captures outer variables by value
$multiplier = 3;
$multiply = fn($x) => $x * $multiplier;
echo $multiply(5); // 15
// traditional closure (multi-line, explicit capture)
$factor = 10;
$scale = function ($x) use ($factor) {
return $x * $factor + 1;
};
// capture by reference
$count = 0;
$increment = function () use (&$count) {
$count++;
};
$increment();
echo $count; // 1
// closures as callbacks
$nums = [1, 2, 3, 4];
$evens = array_filter($nums, fn($n) => $n % 2 === 0);
$doubled = array_map(fn($n) => $n * 2, $nums);Ámbito de Variables y Globales
PHP tiene scope a nivel de función — las variables definidas fuera de una función NO son accesibles dentro sin 'global' o $GLOBALS. Evite 'global' — crea dependencias ocultas y hace el testing difícil. Use inyección de dependencias en su lugar. Las variables static persisten a través de llamadas a función pero están scoped a la función — útiles para caching/memoización pero pueden causar problemas en procesos de larga duración. Los closures deben capturar variables explícitamente con 'use'.
<?php
$global = "I'm global";
function testScope(): void {
// echo $global; // ERROR: not in scope
global $global; // import global
echo $global; // OK
// $GLOBALS superglobal (alternative)
echo $GLOBALS['global'];
}
// static variables: persist across calls
function counter(): int {
static $count = 0;
return ++$count;
}
echo counter(); // 1
echo counter(); // 2
echo counter(); // 3
// closures don't see outer scope by default
$outer = "hello";
$closure = function () {
// echo $outer; // ERROR
};
$closure2 = function () use ($outer) {
echo $outer; // OK, captured
};Declaraciones de Tipo y Strict Types
declare(strict_types=1) debe ser la primera sentencia — aplica verificación estricta de tipos (sin coerción) para todo el archivo. Sin ello, PHP coerce tipos (int 5 pasado a un param string se convierte en '5'). Siempre use strict types en código nuevo. PHP 8+ añade union types, mixed, never (la función nunca devuelve) y static (devuelve la clase). La sintaxis first-class callable (func(...)) crea closures desde cualquier callable — más limpio que referencias a funciones.
<?php
// declare strict types (FIRST line of file)
declare(strict_types=1);
function divide(float $a, float $b): float {
if ($b === 0.0) {
throw new DivisionByZeroError();
}
return $a / $b;
}
// nullable types (?Type or Type|null)
function findUser(?int $id): ?string {
if ($id === null) return null;
return "User $id";
}
// union types (PHP 8+)
function process(int|string $input): int|string {
return is_int($input) ? $input * 2 : strtoupper($input);
}
// return types: void, never, mixed, static
function log(string $msg): void { /* no return */ }
function redirect(): never { header("Location: /"); exit; }
// first-class callable syntax (PHP 8.1+)
$func = strlen(...); // creates Closure from function
echo $func("hello"); // 5Generadores y Yield
Los generadores (funciones con yield) producen valores perezosamente — no calculan todos los valores por adelantado, ahorrando memoria. Esto es esencial para procesar archivos o datasets grandes. yield pausa la función, devolviendo un valor; la función se reanuda cuando se solicita el siguiente valor. Los generadores implementan Iterator, así que funcionan con foreach. Use generadores para: procesamiento de archivos, iteración de filas de base de datos, secuencias infinitas y pipelines. También pueden devolver pares key=>value y aceptar valores vía send().
<?php
// generator: memory-efficient iteration
function readLines(string $file): Generator {
$handle = fopen($file, 'r');
while (($line = fgets($handle)) !== false) {
yield trim($line);
}
fclose($handle);
}
foreach (readLines("large.txt") as $line) {
echo $line; // one line at a time, low memory
}
// infinite generator
function fibonacci(): Generator {
[$a, $b] = [0, 1];
while (true) {
yield $a;
[$a, $b] = [$b, $a + $b];
}
}
// take first 10
$fib = fibonacci();
for ($i = 0; $i < 10; $i++) {
echo $fib->current() . " ";
$fib->next();
}
// yield with key
function pairs(): Generator {
yield 'a' => 1;
yield 'b' => 2;
yield 'c' => 3;
}OOP y Clases
Clase, Propiedades y Constructor
La promoción de constructor de PHP 8 elimina boilerplate — declare propiedades como parámetros de constructor. Visibilidad de propiedad: public (cualquiera), protected (clase + subclases), private (solo clase). readonly (PHP 8.1) previene modificación después de inicialización. self se refiere a la clase actual; static se refiere a la clase llamadora (para late static binding). Use static:: en lugar de self:: en jerarquías de herencia para polimorfismo adecuado.
<?php
class Person {
// typed properties (PHP 7.4+)
public string $name;
protected int $age;
private string $email;
public readonly string $id; // PHP 8.1+
// constructor promotion (PHP 8+)
public function __construct(
string $name,
int $age,
string $email = "",
string $id = ""
) {
$this->name = $name;
$this->age = $age;
$this->email = $email;
$this->id = $id;
}
// methods
public function greet(): string {
return "Hi, I'm {$this->name}";
}
// static method
public static function create(string $name): self {
return new self($name, 0);
}
}
$p = new Person("Alice", 30);
echo $p->greet();
echo Person::create("Bob")->name;Herencia y Clases Abstractas
Las clases abstractas no pueden instanciarse — definen una plantilla para subclases. Los métodos abstractos deben ser implementados por subclases concretas. PHP soporta solo herencia simple (un extends). Use final para prevenir herencia/override cuando la implementación no debería cambiar. Los miembros protected son accesibles en subclases — use para APIs internos. Siempre llame parent::__construct() si el padre tiene un constructor. instanceof verifica tipo: if ($dog instanceof Animal).
<?php
abstract class Animal {
protected string $name;
public function __construct(string $name) {
$this->name = $name;
}
// abstract method: must be implemented by subclasses
abstract public function speak(): string;
// concrete method: inherited as-is
public function describe(): string {
return "{$this->name} says {$this->speak()}";
}
}
class Dog extends Animal {
public function speak(): string {
return "Woof";
}
}
class Cat extends Animal {
public function speak(): string {
return "Meow";
}
}
$dog = new Dog("Rex");
echo $dog->describe(); // Rex says Woof
// final class/method: cannot be extended/overridden
final class Singleton { /* ... */ }Interfaces y Traits
Las interfaces definen contratos — las clases pueden implementar múltiples interfaces (a diferencia de la herencia simple). Todos los métodos de interfaz deben ser public. Los traits proporcionan reutilización de código sin herencia — son 'copy-paste' a nivel de lenguaje. Los traits pueden tener propiedades, métodos e incluso métodos abstractos. Use traits para preocupaciones transversales (timestamps, logging, soft deletes). Resolución de conflictos: use TraitA::method insteadof TraitB cuando los traits tienen mismos nombres de método.
<?php
// interface: contract (no implementation)
interface Comparable {
public function compareTo(object $other): int;
}
interface JsonSerializable {
public function jsonSerialize(): mixed;
}
// a class can implement multiple interfaces
class Product implements Comparable, JsonSerializable {
public function __construct(public float $price) {}
public function compareTo(object $other): int {
return $this->price <=> $other->price;
}
public function jsonSerialize(): mixed {
return ['price' => $this->price];
}
}
// trait: reusable code (horizontal reuse)
trait Timestampable {
public DateTime $createdAt;
public function setCreatedAt(): void {
$this->createdAt = new DateTime();
}
public function age(): DateInterval {
return $this->createdAt->diff(new DateTime());
}
}
class Article {
use Timestampable; // use trait
}Métodos Mágicos
Los métodos mágicos son métodos especiales que interceptan operaciones de objeto. __get/__set implementan sobrecarga de propiedades (propiedades dinámicas). __toString habilita casting a cadena. __invoke hace objetos invocables. __clone se ejecuta al clonar (clone $obj). Úselos con moderación — añaden 'magia' difícil de rastrear. __get/__set son útiles para objetos de transferencia de datos o lazy loading. Siempre documente el comportamiento mágico claramente. PHP 8.2 depreca las propiedades dinámicas — use __get/__set o #[AllowDynamicProperties].
<?php
class Magic {
private array $data = [];
// called when accessing undefined property
public function __get(string $name): mixed {
return $this->data[$name] ?? null;
}
// called when setting undefined property
public function __set(string $name, mixed $value): void {
$this->data[$name] = $value;
}
// called when isset() or empty() on undefined property
public function __isset(string $name): bool {
return isset($this->data[$name]);
}
// called when object is used as string
public function __toString(): string {
return json_encode($this->data);
}
// called when object is called as function
public function __invoke(string $arg): string {
return "Called with: $arg";
}
// called on clone
public function __clone(): void {
$this->data = []; // reset on clone
}
}
$m = new Magic();
$m->foo = "bar"; // __set
echo $m->foo; // __get -> "bar"
echo $m; // __toString -> {"foo":"bar"}
echo $m("test"); // __invokeNamespaces y Autoloading
Los namespaces previenen colisiones de nombres de clase — como paquetes en Java. El namespace debe ser la primera sentencia. use importa clases (con aliases opcionales: use Foo\Bar as B). El autoloading PSR-4 mapea namespaces a rutas de archivo: App\Models\User → src/Models/User.php. El autoloader de Composer (require 'vendor/autoload.php') maneja esto automáticamente. Siempre use namespaces en PHP moderno. El \\ en cadenas es un backslash escapado (separador de namespace).
<?php
// file: src/Models/User.php
namespace App\Models;
use App\Database\Connection;
use App\Exceptions\UserNotFoundException;
class User {
private Connection $db;
public function __construct(Connection $db) {
$this->db = $db;
}
public function find(int $id): ?self {
// ...
throw new UserNotFoundException("User $id not found");
}
}
// composer.json (PSR-4 autoloading)
// {
// "autoload": {
// "psr-4": { "App\\": "src/" }
// }
// }
// usage
use App\Models\User;
$user = new User($db);Web, Formularios y File I/O
Manejo de Formularios y Validación
Siempre valide del lado del servidor — la validación del lado del cliente es para UX, no seguridad. filter_input/filter_var con FILTER_VALIDATE_* devuelven false en entrada inválida. Recorte cadenas antes de validar. Use sentencias preparadas para inserciones de base de datos. Los tokens CSRF previenen cross-site request forgery — genere por sesión y verifique en POST. bin2hex(random_bytes(32)) genera un token criptográficamente seguro. Nunca confíe en entrada de usuario — valide, sanitize y escape.
<?php
// process POST form
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$name = trim($_POST['name'] ?? '');
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
$age = filter_var($_POST['age'] ?? 0, FILTER_VALIDATE_INT);
$errors = [];
if (empty($name)) {
$errors[] = "Name is required";
} elseif (strlen($name) > 100) {
$errors[] = "Name too long";
}
if ($email === false) {
$errors[] = "Valid email required";
}
if ($age === false || $age < 18) {
$errors[] = "Must be 18+";
}
if (empty($errors)) {
// process valid data
// save to database, redirect, etc.
header("Location: /success");
exit;
}
}
// CSRF token
session_start();
$token = bin2hex(random_bytes(32));
$_SESSION['csrf'] = $token;
// in form: <input type="hidden" name="csrf" value="<?= $token ?>">Sesiones y Cookies
Las sesiones almacenan datos del lado del servidor (identificadas por una cookie de ID de sesión). session_start() debe llamarse antes de cualquier salida (o use ob_start()). Almacene datos mínimos en sesiones — consumen memoria del servidor. Para cookies, siempre establezca secure (solo HTTPS), httponly (prevenir acceso XSS) y samesite (protección CSRF). Destruya sesiones adecuadamente: unset variables, destruya sesión, limpie cookie. Para apps escalables, use un session handler respaldado por Redis/base de datos en lugar de archivos.
<?php
// start session (must be before any output)
session_start();
// set session data
$_SESSION['user_id'] = 42;
$_SESSION['username'] = "Alice";
$_SESSION['login_time'] = time();
// read session data
$userId = $_SESSION['user_id'] ?? null;
// check if logged in
function isLoggedIn(): bool {
return isset($_SESSION['user_id']);
}
// destroy session
session_unset(); // clear variables
session_destroy(); // destroy session
setcookie(session_name(), '', time() - 3600, '/'); // clear cookie
// cookies
setcookie("theme", "dark", [
'expires' => time() + 86400 * 30, // 30 days
'path' => '/',
'secure' => true, // HTTPS only
'httponly' => true, // not accessible via JS
'samesite' => 'Strict', // CSRF protection
]);
// read cookie
$theme = $_COOKIE['theme'] ?? 'light';File I/O
file_get_contents/file_put_contents son convenientes para archivos pequeños. Para archivos grandes, use fopen/fread/fwrite con streams. fgetcsv/fputcsv manejan formato CSV (incluyendo comillado/escaping). json_decode con true devuelve arrays asociativos (objetos por defecto). Siempre verifique file_exists y maneje errores (permisos, disco lleno). Para subidas de archivos, use move_uploaded_file() por seguridad. Bloquee archivos con flock() al escribir concurrentemente.
<?php
// read entire file
$content = file_get_contents("data.txt");
$lines = file("data.txt", FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
// write file
file_put_contents("output.txt", "Hello World");
file_put_contents("log.txt", "entry\n", FILE_APPEND); // append
// CSV
$csv = fopen("data.csv", "r");
while (($row = fgetcsv($csv)) !== false) {
print_r($row); // array of column values
}
fclose($csv);
// write CSV
$out = fopen("export.csv", "w");
fputcsv($out, ["Name", "Email", "Age"]);
fputcsv($out, ["Alice", "[email protected]", 30]);
fclose($out);
// JSON
$data = json_decode(file_get_contents("config.json"), true); // assoc array
file_put_contents("config.json", json_encode($data, JSON_PRETTY_PRINT));
// file info
file_exists("data.txt"); // bool
filesize("data.txt"); // bytes
filemtime("data.txt"); // modification timestamp
is_dir("folder"); // boolSubidas de Archivos
Las subidas de archivos vienen a través de $_FILES, no $_POST. Siempre valide: verifique código de error, verifique tipo MIME con finfo (no $_FILES['type'] que es proporcionado por el cliente y falsificable), aplique límites de tamaño y genere nombres de archivo seguros (nunca confíe en el nombre original). move_uploaded_file() es una función de seguridad — verifica que el archivo se subió vía HTTP POST. Almacene subidas fuera del web root o sirva a través de PHP para prevenir acceso directo. Considere escanear subidas en busca de malware.
<?php
// HTML: <form method="POST" enctype="multipart/form-data">
// <input type="file" name="document">
// <input type="submit">
// </form>
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$file = $_FILES['document'];
// $file contains:
// ['name'] => original filename
// ['type'] => MIME type (unreliable!)
// ['tmp_name'] => temporary path
// ['error'] => UPLOAD_ERR_OK (0) on success
// ['size'] => file size in bytes
if ($file['error'] !== UPLOAD_ERR_OK) {
die("Upload error: " . $file['error']);
}
// validate
$allowedTypes = ['application/pdf', 'image/jpeg', 'image/png'];
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($file['tmp_name']);
if (!in_array($mimeType, $allowedTypes)) {
die("Invalid file type");
}
if ($file['size'] > 5 * 1024 * 1024) { // 5MB
die("File too large");
}
// move to permanent location
$dest = "uploads/" . uniqid() . "_" . $file['name'];
move_uploaded_file($file['tmp_name'], $dest);
echo "Uploaded to: $dest";
}cURL y Peticiones HTTP
cURL es el cliente HTTP estándar en PHP — maneja HTTPS, redirecciones, cookies y autenticación. Siempre establezca CURLOPT_RETURNTRANSFER para obtener la respuesta como cadena (de lo contrario se imprime). Establezca timeouts para evitar colgadas. Para peticiones simples, file_get_contents con stream_context funciona pero carece de características. Para producción, use Guzzle (composer require guzzlehttp/guzzle) o Symfony HTTP Client — ofrecen mejores APIs, lógica de retry y cumplimiento PSR-18.
<?php
// GET request
$ch = curl_init("https://api.example.com/users");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200) {
$data = json_decode($response, true);
print_r($data);
}
// POST request with JSON
$ch = curl_init("https://api.example.com/users");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['name' => 'Alice']),
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $token,
],
]);
$response = curl_exec($ch);
curl_close($ch);
// simpler: file_get_contents with context
$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => "Content-Type: application/json\r\n",
'content' => json_encode(['name' => 'Alice']),
],
]);
$result = file_get_contents("https://api.example.com/users", false, $context);Base de Datos (PDO)
Conexión PDO y Fundamentos
PDO (PHP Data Objects) es la capa estándar de abstracción de base de datos — soporta MySQL, PostgreSQL, SQLite y más. Siempre establezca ERRMODE_EXCEPTION para manejo de errores adecuado y ATTR_EMULATE_PREPARES=false para sentencias preparadas reales (mejor seguridad). FETCH_ASSOC devuelve arrays asociativos (use FETCH_OBJ para objetos, FETCH_CLASS para mapear a clases). Siempre use charset utf8mb4 para soporte Unicode completo (incluyendo emoji). Almacene la conexión en un singleton o contenedor DI.
<?php
// connect (always use exception mode)
$dsn = "mysql:host=localhost;dbname=test;charset=utf8mb4";
$pdo = new PDO($dsn, "username", "password", [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false, // use real prepared statements
]);
// simple query
$stmt = $pdo->query("SELECT * FROM users LIMIT 5");
$users = $stmt->fetchAll(); // array of associative arrays
// fetch one row
$user = $pdo->query("SELECT * FROM users WHERE id = 1")->fetch();
// fetch column
$count = $pdo->query("SELECT COUNT(*) FROM users")->fetchColumn();
// execute (no params)
$pdo->exec("DELETE FROM logs WHERE created_at < '2023-01-01'");
$deleted = $pdo->rowCount(); // affected rowsSentencias Preparadas (Prevención de Inyección SQL)
Las sentencias preparadas son OBLIGATORIAS para cualquier consulta con entrada de usuario — separan la estructura SQL de los datos, haciendo la inyección imposible. Use ? para posicionales o :name para parámetros nombrados. Para cláusulas IN, debe construir la cadena de placeholders dinámicamente (pero los valores siguen parametrizados). lastInsertId() devuelve el último valor auto-increment. Nunca concatene entrada de usuario en SQL — incluso con funciones de escape. Esta es la regla #1 de seguridad en PHP.
<?php
// prepared statements: THE way to prevent SQL injection
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = ? AND status = ?");
$stmt->execute([$email, 'active']);
$user = $stmt->fetch();
// named parameters (more readable)
$stmt = $pdo->prepare(
"INSERT INTO users (name, email, age) VALUES (:name, :email, :age)"
);
$stmt->execute([
':name' => 'Alice',
':email' => '[email protected]',
':age' => 30,
]);
$id = $pdo->lastInsertId(); // get auto-increment ID
// IN clause with prepared statements
$ids = [1, 2, 3, 4];
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($placeholders)");
$stmt->execute($ids);
$users = $stmt->fetchAll();
// NEVER do this (SQL injection!):
// $pdo->query("SELECT * FROM users WHERE name = '$_GET[name]'");Transacciones y Manejo de Errores
Las transacciones aseguran atomicidad — todas las operaciones tienen éxito o todas fallan. beginTransaction/commit/rollBack envuelven la unidad de trabajo. Siempre envuelva transacciones en try/catch y haga roll back en cualquier excepción. PDO lanza PDOException en errores (con ERRMODE_EXCEPTION). Mantenga transacciones cortas para reducir contención de locks. Para transacciones anidadas, use savepoints o un transaction manager. Nunca deje una transacción abierta — siempre commit o roll back.
<?php
try {
$pdo->beginTransaction();
$pdo->prepare("UPDATE accounts SET balance = balance - ? WHERE id = ?")
->execute([$amount, $fromId]);
$pdo->prepare("UPDATE accounts SET balance = balance + ? WHERE id = ?")
->execute([$amount, $toId]);
// log transaction
$pdo->prepare("INSERT INTO transfers (from_id, to_id, amount) VALUES (?, ?, ?)")
->execute([$fromId, $toId, $amount]);
$pdo->commit();
echo "Transfer complete";
} catch (PDOException $e) {
$pdo->rollBack(); // undo all changes
error_log("Transfer failed: " . $e->getMessage());
throw new RuntimeException("Transfer failed", 0, $e);
}
// check if in transaction
if ($pdo->inTransaction()) {
$pdo->commit();
}Patrones de Fetching de Datos
Elija el modo de fetch adecuado para su caso. FETCH_ASSOC es el más común (array con nombres de columna). FETCH_CLASS mapea filas a objetos — excelente para modelos de dominio. FETCH_KEY_PAIR crea mapas id=>value (para dropdowns). FETCH_GROUP agrupa filas por la primera columna — útil para relaciones uno-a-muchos. Para conjuntos de resultados grandes, use fetch() en un bucle en lugar de fetchAll() para ahorrar memoria. Siempre cierre cursores con $stmt->closeCursor() cuando termine.
<?php
$stmt = $pdo->prepare("SELECT id, name, email FROM users WHERE active = 1");
$stmt->execute();
// fetch modes
$row = $stmt->fetch(PDO::FETCH_ASSOC); // ['id' => 1, 'name' => 'Alice']
$row = $stmt->fetch(PDO::FETCH_NUM); // [1, 'Alice', '[email protected]']
$row = $stmt->fetch(PDO::FETCH_BOTH); // both (default)
$obj = $stmt->fetch(PDO::FETCH_OBJ); // stdClass with properties
// fetch all
$all = $stmt->fetchAll(PDO::FETCH_ASSOC);
// fetch into class
class User {
public int $id;
public string $name;
}
$stmt->setFetchMode(PDO::FETCH_CLASS, User::class);
$users = $stmt->fetchAll(); // array of User objects
// fetch key-value pairs
$pairs = $pdo->query("SELECT id, name FROM users")
->fetchAll(PDO::FETCH_KEY_PAIR); // [1 => 'Alice', 2 => 'Bob']
// fetch grouped
$grouped = $pdo->query("SELECT dept, name FROM employees")
->fetchAll(PDO::FETCH_GROUP | PDO::FETCH_ASSOC);
// ['IT' => [['name' => 'Alice']], 'HR' => [['name' => 'Bob']]]Mejores Prácticas de Base de Datos
El patrón repository separa el acceso a datos de la lógica de negocio — haciendo el código testeable (mockee el PDO) y mantenible. Use inyección de dependencias para pasar la conexión PDO. Nunca cree nuevas conexiones PDO por consulta — reutilice una única conexión (o pool). Para apps de alto tráfico, considere un connection pooler (ProxySQL para MySQL, PgBouncer para PostgreSQL). Siempre perfile consultas lentas con EXPLAIN y añada índices apropiados. Considere un ORM (Doctrine, Eloquent) para dominios complejos.
<?php
// 1. Connection singleton (or use DI container)
class Database {
private static ?PDO $instance = null;
public static function conn(): PDO {
if (self::$instance === null) {
self::$instance = new PDO(
"mysql:host=localhost;dbname=app;charset=utf8mb4",
"user", "pass",
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
);
}
return self::$instance;
}
}
// 2. Repository pattern
class UserRepository {
public function __construct(private PDO $db) {}
public function findById(int $id): ?array {
$stmt = $this->db->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$id]);
$user = $stmt->fetch();
return $user ?: null;
}
}
// 3. Always close statements (or let them go out of scope)
// 4. Use LIMIT for queries that might return huge results
// 5. Index columns used in WHERE, JOIN, ORDER BY
// 6. Use EXPLAIN to analyze slow queriesFecha/Hora y Seguridad
Fecha y Hora
Las funciones de fecha de PHP usan la zona horaria del servidor por defecto — siempre establezca date_default_timezone_set('Asia/Shanghai') o use DateTimeZone explícitamente. La clase DateTime es orientada a objetos y maneja zonas horarias, intervalos y formateo mejor que funciones procedurales. strtotime() parsea descripciones de fecha en inglés ('next Monday', '+1 month') — conveniente pero puede ser sorprendente en límites de mes. Para matemática de fechas, use DateTime::diff() y DateInterval. Siempre almacene fechas en UTC en bases de datos.
<?php
// current time
$now = date('Y-m-d H:i:s'); // 2024-06-15 14:30:00
$timestamp = time(); // Unix timestamp
$dt = new DateTime(); // DateTime object
// formatting
echo date('Y-m-d'); // 2024-06-15
echo date('d/m/Y H:i:s'); // 15/06/2024 14:30:00
echo date('l, F j, Y'); // Saturday, June 15, 2024
// create from string
$dt = new DateTime('2024-01-15');
$dt = DateTime::createFromFormat('d/m/Y', '15/01/2024');
// modify dates
$dt->modify('+1 month');
$dt->modify('-2 days');
$tomorrow = date('Y-m-d', strtotime('tomorrow'));
$nextWeek = date('Y-m-d', strtotime('+1 week'));
// difference
$start = new DateTime('2024-01-01');
$end = new DateTime('2024-12-31');
$diff = $start->diff($end);
echo $diff->days; // 365
// timezone
$dt = new DateTime('now', new DateTimeZone('Asia/Shanghai'));
$dt->setTimezone(new DateTimeZone('UTC'));Hashing de Contraseñas y Seguridad
password_hash() usa bcrypt (o Argon2 si disponible) con generación automática de salt — nunca haga su propio hashing. password_verify() verifica contraseñas contra hashes de forma segura (comparación de tiempo constante para prevenir timing attacks). password_needs_rehash() le permite actualizar hashes cuando aumenta el factor de coste. Para tokens aleatorios (CSRF, claves de API, resets de contraseña), siempre use random_bytes() — no rand() o mt_rand() que son predecibles. Use hash_hmac para autenticación de mensajes.
<?php
// hash password (bcrypt by default)
$hash = password_hash("mypassword", PASSWORD_DEFAULT);
// $2y$10$... (includes algorithm, cost, salt)
// verify password
if (password_verify($input, $hash)) {
echo "Valid password";
}
// check if hash needs rehash (algorithm upgrade)
if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
$newHash = password_hash($input, PASSWORD_DEFAULT);
// update stored hash
}
// NEVER use md5() or sha1() for passwords!
// NEVER store plaintext passwords!
// generate secure random
$token = bin2hex(random_bytes(32)); // 64-char hex string
$bytes = random_bytes(16); // raw bytes
$int = random_int(1, 1000); // cryptographically secure
// hash for integrity (not passwords)
$checksum = hash('sha256', $data);
$hmac = hash_hmac('sha256', $data, $secretKey);Escape de Salida y Prevención XSS
XSS es la vulnerabilidad web #1 — siempre escape la salida según el contexto. htmlspecialchars() para HTML (ENT_QUOTES escapa tanto comillas simples como dobles). urlencode() para URLs. json_encode() con flags hex para contextos JavaScript. Nunca confíe en entrada de usuario — escape en salida, no en entrada (puede necesitar los datos raw en otro lugar). Establezca cabeceras Content-Security-Policy como defensa en profundidad. Considere un motor de plantillas (Twig, Blade) que auto-escapa por defecto.
<?php
// XSS: Cross-Site Scripting — always escape output!
$name = $_GET['name']; // could be: <script>alert('xss')</script>
// HTML context
echo htmlspecialchars($name, ENT_QUOTES, 'UTF-8');
// converts < > " ' & to HTML entities
// in HTML template
?>
<p>Hello, <?= htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?></p>
<input value="<?= htmlspecialchars($value, ENT_QUOTES, 'UTF-8') ?>">
<?php
// URL context
$url = "https://example.com/search?q=" . urlencode($query);
// JavaScript context
$json = json_encode($data, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT);
?>
<script>
var data = <?= $json ?>;
</script>
<?php
// Content Security Policy header
header("Content-Security-Policy: default-src 'self'; script-src 'self'");JSON y Respuestas API
json_encode/decode son las funciones JSON estándar. Siempre establezca Content-Type: application/json para respuestas API. Use JSON_UNESCAPED_UNICODE para mantener chino/emoji legible (de lo contrario se convierten en \uXXXX). json_decode con true devuelve arrays asociativos (más común en PHP). Siempre verifique json_last_error() después de decodificar JSON no confiable. Para APIs REST, establezca códigos de estado HTTP apropiados (200, 201, 400, 404, 500) y use estructura de respuesta consistente.
<?php
// encode PHP array/object to JSON
$data = ['name' => 'Alice', 'age' => 30, 'hobbies' => ['reading', 'coding']];
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
// {"name": "Alice", "age": 30, "hobbies": ["reading", "coding"]}
// decode JSON to PHP
$obj = json_decode($json); // stdClass object
$arr = json_decode($json, true); // associative array
// handle errors
$data = json_decode($badJson, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException("JSON error: " . json_last_error_msg());
}
// API response
header('Content-Type: application/json; charset=utf-8');
http_response_code(200);
echo json_encode([
'status' => 'success',
'data' => $users,
'meta' => ['page' => 1, 'total' => 100],
], JSON_UNESCAPED_UNICODE);
// API request handling
$input = json_decode(file_get_contents('php://input'), true);Composer y Gestión de Dependencias
Composer es el gestor de paquetes de PHP — esencial para PHP moderno. require especifica dependencias de producción; require-dev para desarrollo (tests, etc.). El autoloading PSR-4 mapea namespaces a directorios. Siempre commitee composer.json y composer.lock (bloquea versiones exactas). Use composer install (desde lock) en producción, composer update para obtener lo último. Paquetes populares: Monolog (logging), Guzzle (HTTP), PHPUnit (testing), componentes Symfony, framework Laravel. Nunca commitee el directorio vendor/.
<?php
// composer.json
// {
// "require": {
// "monolog/monolog": "^3.0",
// "guzzlehttp/guzzle": "^7.0"
// },
// "autoload": {
// "psr-4": {"App\\": "src/"}
// }
// }
// install: composer install
// update: composer update
// add: composer require monolog/monolog
// autoload (at the top of your app)
require 'vendor/autoload.php';
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
$log = new Logger('app');
$log->pushHandler(new StreamHandler('app.log', Logger::WARNING));
$log->warning('User not found', ['user_id' => 42]);
// environment variables (vlucas/phpdotenv)
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
$dbHost = $_ENV['DB_HOST'];Desarrollo de APIs REST
Manejo de Peticiones y Respuestas JSON
Las APIs REST intercambian JSON. A diferencia de los envíos de formularios (que pueblan $_POST), las peticiones JSON deben leerse de php://input y decodificarse con json_decode. Siempre establezca Content-Type: application/json para respuestas y use http_response_code() para códigos de estado HTTP apropiados. Valide toda entrada — json_decode no garantiza la estructura esperada. Use el operador null coalescing (??) para acceso seguro. JSON_PRETTY_PRINT es útil para depuración pero omítalo en producción para payloads más pequeños.
// Get JSON from request body (not $_POST for JSON!)
$json = file_get_contents('php://input');
$data = json_decode($json, true); // true = associative array
if (json_last_error() !== JSON_ERROR_NONE) {
http_response_code(400);
echo json_encode(['error' => 'Invalid JSON']);
exit;
}
// Process the data
$name = $data['name'] ?? '';
$email = filter_var($data['email'] ?? '', FILTER_VALIDATE_EMAIL);
// Return JSON response
header('Content-Type: application/json');
http_response_code(200); // 200 OK, 201 Created, 400 Bad Request, etc.
echo json_encode([
'success' => true,
'data' => ['id' => 1, 'name' => $name],
'message' => 'User created'
], JSON_PRETTY_PRINT);Routing y Métodos HTTP
Las APIs REST mapean métodos HTTP a operaciones CRUD: GET (leer), POST (crear), PUT/PATCH (actualizar), DELETE (eliminar). El routing coincide el método + ruta URL a un handler. Use preg_match para rutas parametrizadas (p. ej., /api/users/42). En producción, use una biblioteca de router (FastRoute, Symfony Routing) o framework (Laravel, Slim) para routing más limpio, middleware e inyección de dependencias. Siempre devuelva códigos de estado HTTP apropiados: 200 (OK), 201 (Created), 204 (No Content), 400 (Bad Request), 404 (Not Found), 500 (Server Error).
// Simple REST router based on method + path
$method = $_SERVER['REQUEST_METHOD'];
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$path = trim($path, '/');
// Route table: method => [pattern => handler]
switch (true) {
case ($method === 'GET' && $path === 'api/users'):
echo json_encode(getAllUsers());
break;
case ($method === 'GET' && preg_match('#^api/users/(\d+)$#', $path, $m)):
echo json_encode(getUser((int)$m[1]));
break;
case ($method === 'POST' && $path === 'api/users'):
$data = json_decode(file_get_contents('php://input'), true);
echo json_encode(createUser($data));
http_response_code(201);
break;
case ($method === 'PUT' && preg_match('#^api/users/(\d+)$#', $path, $m)):
$data = json_decode(file_get_contents('php://input'), true);
echo json_encode(updateUser((int)$m[1], $data));
break;
case ($method === 'DELETE' && preg_match('#^api/users/(\d+)$#', $path, $m)):
deleteUser((int)$m[1]);
http_response_code(204); // No Content
break;
default:
http_response_code(404);
echo json_encode(['error' => 'Not found']);
}Autenticación API (JWT)
JWT habilita autenticación stateless — el servidor no necesita almacenar sesiones. El token contiene un payload (ID de usuario, expiración) firmado con una clave secreta. El cliente envía el token en la cabecera Authorization (Bearer token). El servidor verifica la firma para asegurar que el token no fue manipulado. JWT es excelente para APIs y microservicios (sin store de sesión compartido necesario). Sin embargo, los JWTs no pueden revocarse antes de la expiración — use tiempos de expiración cortos y una estrategia de refresh token. En producción, use la biblioteca firebase/php-jwt en lugar de implementar criptografía usted mismo. Nunca almacene datos sensibles en el payload JWT — está solo base64-encoded, no encriptado.
// JWT (JSON Web Token) authentication flow
// 1. Login: verify credentials, issue JWT
function login($email, $password) {
$user = findUserByEmail($email);
if ($user && password_verify($password, $user['password_hash'])) {
// Create JWT: header.payload.signature
$header = base64url_encode(json_encode(['alg' => 'HS256', 'typ' => 'JWT']));
$payload = base64url_encode(json_encode([
'user_id' => $user['id'],
'exp' => time() + 3600 // expires in 1 hour
]));
$signature = hash_hmac('sha256', "$header.$payload", SECRET_KEY, true);
$jwt = "$header.$payload." . base64url_encode($signature);
return $jwt;
}
return null;
}
// 2. Verify JWT on protected routes
function verifyJWT($token) {
$parts = explode('.', $token);
if (count($parts) !== 3) return false;
[$header, $payload, $signature] = $parts;
$expected = base64url_encode(
hash_hmac('sha256', "$header.$payload", SECRET_KEY, true)
);
if (!hash_equals($expected, $signature)) return false;
$data = json_decode(base64url_decode($payload), true);
return ($data['exp'] ?? 0) > time() ? $data : false;
}
// Use firebase/php-jwt library in production!Validación y Sanitización de Entrada
La validación de entrada es crítica para la seguridad de APIs. filter_var de PHP proporciona validadores integrados (FILTER_VALIDATE_EMAIL, FILTER_VALIDATE_INT, FILTER_VALIDATE_URL) con opciones como rango min/max. Siempre valide del lado del servidor — la validación del lado del cliente es para UX, no seguridad. Sanitize cadenas con htmlspecialchars para prevenir XSS al salir HTML. Para APIs JSON, devuelva 422 (Unprocessable Entity) para errores de validación con mensajes descriptivos. Considere usar una biblioteca de validación (Respect/Validation, Symfony Validator) para reglas complejas. Nunca confíe en entrada de usuario — valide tipo, longitud, formato y reglas de negocio.
// Validate and sanitize API input
function validateUserInput(array $data): array {
$errors = [];
// Required fields
if (empty($data['name'])) {
$errors[] = 'Name is required';
}
// Email validation
$email = filter_var($data['email'] ?? '', FILTER_VALIDATE_EMAIL);
if (!$email) {
$errors[] = 'Valid email is required';
}
// Age: integer between 18 and 120
$age = filter_var($data['age'] ?? null, FILTER_VALIDATE_INT, [
'options' => ['min_range' => 18, 'max_range' => 120]
]);
if ($age === false) {
$errors[] = 'Age must be between 18 and 120';
}
// String sanitization (remove tags, trim)
$name = htmlspecialchars(trim($data['name'] ?? ''), ENT_QUOTES, 'UTF-8');
// URL validation
$website = filter_var($data['website'] ?? '', FILTER_VALIDATE_URL);
return ['errors' => $errors, 'data' => compact('email', 'age', 'name')];
}
$result = validateUserInput($input);
if (!empty($result['errors'])) {
http_response_code(422); // Unprocessable Entity
echo json_encode(['errors' => $result['errors']]);
exit;
}CORS (Cross-Origin Resource Sharing)
CORS controla qué dominios pueden acceder a su API desde un navegador. Los navegadores envían una petición preflight OPTIONS para peticiones no simples (PUT/DELETE, cabeceras personalizadas). Su servidor debe responder con las cabeceras Access-Control-Allow-* apropiadas. Por seguridad, especifique orígenes exactos en lugar de '*' (especialmente con credenciales). Access-Control-Allow-Credentials: true se necesita si la API usa cookies o cabeceras Authorization. Vary: Origin le dice a los caches que la respuesta varía por origen. CORS mal configurado puede exponer su API a cualquier sitio web — siempre haga whitelist de orígenes confiables.
// Enable CORS for API requests from browsers
header('Access-Control-Allow-Origin: https://example.com'); // specific origin
// OR: header('Access-Control-Allow-Origin: *'); // any origin (less secure)
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Access-Control-Allow-Credentials: true'); // for cookies/auth
header('Access-Control-Max-Age: 86400'); // cache preflight for 24h
// Handle preflight OPTIONS request
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
// Dynamic origin (check against whitelist)
$allowedOrigins = ['https://app.example.com', 'https://admin.example.com'];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowedOrigins)) {
header('Access-Control-Allow-Origin: ' . $origin);
header('Vary: Origin'); // important for caching
}Seguridad (XSS, CSRF, Inyección SQL)
Prevenir XSS (Cross-Site Scripting)
XSS ocurre cuando datos no confiables se insertan en HTML sin escape, permitiendo a atacantes ejecutar JavaScript en los navegadores de las víctimas. La solución: siempre escape la salida con htmlspecialchars (convierte <, >, &, ", ' a entidades HTML). Diferentes contextos necesitan diferente escape: cuerpo HTML (htmlspecialchars), atributos HTML (htmlspecialchars con ENT_QUOTES), JavaScript (json_encode), URLs (urlencode). Las cabeceras Content Security Policy (CSP) añaden defensa en profundidad restringiendo de dónde pueden cargarse scripts. Nunca use eval(), innerHTML o document.write() con entrada de usuario. Frameworks como Twig y Blade auto-escapan por defecto.
// XSS: attacker injects malicious JavaScript into your page
// BAD: outputting user input without escaping
echo "<p>" . $_GET['name'] . "</p>";
// If name = <script>alert('xss')</script>, it executes!
// GOOD: escape output with htmlspecialchars
echo "<p>" . htmlspecialchars($_GET['name'], ENT_QUOTES, 'UTF-8') . "</p>";
// & < > " ' are converted to HTML entities
// For HTML attributes:
echo '<input value="' . htmlspecialchars($value, ENT_QUOTES) . '">';
// For JavaScript context (JSON encode):
echo '<script>var name = ' . json_encode($name) . ';</script>';
// For URL parameters:
echo '<a href="?q=' . urlencode($query) . '">Search</a>';
// Content Security Policy (CSP) header — defense in depth
header("Content-Security-Policy: default-src 'self'; script-src 'self'");
// Use prepared statements (prevents SQL injection too)Prevenir CSRF (Cross-Site Request Forgery)
CSRF engaña al navegador de un usuario autenticado para enviar una petición a su sitio (p. ej., una transferencia de dinero) sin su conocimiento. La defensa: incluya un token impredecible en formularios que el atacante no pueda adivinar. El token se almacena en la sesión y se verifica al enviar. Use hash_equals() para comparación timing-safe (previene timing attacks). Para llamadas AJAX/API, las cookies SameSite=Strict y requerir cabeceras personalizadas (como X-Requested-With) proporcionan protección. Las peticiones GET nunca deberían modificar datos (pueden ser disparadas por tags de imagen o enlaces). Frameworks como Laravel y Symfony tienen middleware CSRF integrado.
// CSRF: attacker tricks a logged-in user into submitting a form
// to your site (using their session cookie)
// Defense: anti-CSRF tokens
session_start();
// Generate a token (once per session)
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
// Include token in forms as a hidden field
echo '<form method="POST" action="/transfer">';
echo '<input type="hidden" name="csrf_token" value="' . $_SESSION['csrf_token'] . '">';
echo '<input type="text" name="amount">';
echo '<button type="submit">Transfer</button>';
echo '</form>';
// Verify token on POST/PUT/DELETE requests
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$token = $_POST['csrf_token'] ?? '';
if (!hash_equals($_SESSION['csrf_token'], $token)) {
http_response_code(403);
die('CSRF token validation failed');
}
// Process the form...
}
// For APIs: use SameSite cookies + custom headers
// (browsers block cross-origin requests without explicit CORS permission)Prevenir Inyección SQL
La inyección SQL es la vulnerabilidad web #1 — permite a atacantes leer/modificar/eliminar toda su base de datos. La solución universal: sentencias preparadas (consultas parametrizadas). La estructura de la consulta y los datos se envían por separado, así que la entrada de usuario nunca puede interpretarse como SQL. Nunca concatene entrada de usuario en consultas. prepare/execute de PDO maneja el escape automáticamente. Vincule parámetros con sus tipos (PDO::PARAM_INT, PDO::PARAM_STR). Para cláusulas IN con items variables, genere placeholders dinámicamente. Establezca PDO::ATTR_EMULATE_PREPARES a false para sentencias preparadas reales del lado del servidor (mejor seguridad).
// SQL injection: attacker manipulates queries via unescaped input
// BAD: string concatenation
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = $id";
// If id = "1 OR 1=1", returns ALL users!
// If id = "1; DROP TABLE users; --", deletes the table!
// GOOD: prepared statements with parameter binding
$pdo = new PDO('mysql:host=localhost;dbname=myapp', $user, $pass);
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); // real prepared statements
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
$stmt->execute();
$user = $stmt->fetch(PDO::FETCH_ASSOC);
// Multiple parameters:
$stmt = $pdo->prepare('INSERT INTO users (name, email, age) VALUES (:name, :email, :age)');
$stmt->execute([
':name' => $name,
':email' => $email,
':age' => $age
]);
// IN clause with variable arguments:
$ids = [1, 2, 3];
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($placeholders)");
$stmt->execute($ids);Hashing de Contraseñas y Autenticación
password_hash() usa bcrypt (o Argon2) con un salt aleatorio — el estándar de la industria para almacenamiento de contraseñas. El salt está embebido en el hash, así que no lo gestiona por separado. password_verify() compara de forma segura la entrada contra el hash almacenado (timing-safe). password_needs_rehash() le permite actualizar hashes cuando aumenta factores de coste o cambia algoritmos — verifica si el hash coincide con la configuración actual y re-hashea en el próximo login. Nunca use MD5, SHA1 o texto plano para contraseñas — son trivialmente crackeables. Aplique políticas de contraseñas fuertes pero prefiera longitud sobre complejidad (NIST recomienda 8+ caracteres mínimo).
// NEVER store plain-text or MD5/SHA1 passwords!
// Use password_hash() (bcrypt/argon2 by default)
// Hash a password (on registration)
$password = $_POST['password'];
$hash = password_hash($password, PASSWORD_DEFAULT);
// PASSWORD_DEFAULT = bcrypt (or argon2id in PHP 7.3+)
// Store $hash in the database
// Verify a password (on login)
if (password_verify($inputPassword, $storedHash)) {
// Password is correct
session_regenerate_id(true);
$_SESSION['user_id'] = $user['id'];
} else {
echo "Invalid credentials";
}
// Rehash if algorithm was upgraded (migration)
if (password_verify($input, $hash) &&
password_needs_rehash($hash, PASSWORD_DEFAULT)) {
$newHash = password_hash($input, PASSWORD_DEFAULT);
// Update database with $newHash
}
// Password requirements validation
if (strlen($password) < 8 ||
!preg_match('/[A-Z]/', $password) ||
!preg_match('/[a-z]/', $password) ||
!preg_match('/[0-9]/', $password)) {
echo "Password must be 8+ chars with upper, lower, and number";
}Seguridad de Subida de Archivos
Las subidas de archivos son un vector de ataque mayor. Nunca confíe en $_FILES['type'] (establecido por el navegador, fácilmente falsificable) — use finfo para detectar el tipo MIME real. Nunca use el nombre de archivo proporcionado por el usuario (podría contener path traversal como ../../script.php) — genere un nombre aleatorio. Almacene subidas fuera del web root o en un directorio con ejecución PHP deshabilitada. Para imágenes, re-encódelas (imagecreatefromjpeg + imagejpeg) para eliminar código PHP embebido oculto en datos EXIF. Limite el tamaño de archivo para prevenir denegación de servicio. Valide extensión, tipo MIME y magic bytes. Considere escanear subidas con un antivirus (ClamAV) para seguridad adicional.
// Secure file upload handling
$upload = $_FILES['avatar'];
// 1. Check for upload errors
if ($upload['error'] !== UPLOAD_ERR_OK) {
die('Upload failed: error ' . $upload['error']);
}
// 2. Validate file size
$maxSize = 2 * 1024 * 1024; // 2MB
if ($upload['size'] > $maxSize) {
die('File too large (max 2MB)');
}
// 3. Validate MIME type (don't trust $_FILES['type']!)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($upload['tmp_name']);
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (!in_array($mimeType, $allowedTypes)) {
die('Invalid file type');
}
// 4. Generate a safe filename (never use user-supplied name)
$safeName = bin2hex(random_bytes(16)) . '.jpg';
// 5. Store OUTSIDE the web root (or in a non-executable dir)
$dest = __DIR__ . '/uploads/' . $safeName;
if (!move_uploaded_file($upload['tmp_name'], $dest)) {
die('Failed to save file');
}
// 6. For images: re-encode to strip malicious metadata
$img = imagecreatefromjpeg($dest);
imagejpeg($img, $dest, 90); // re-encode strips embedded PHP/scriptscURL y Peticiones HTTP
cURL Básico GET y POST
cURL es el cliente HTTP más potente de PHP, soportando GET, POST, métodos personalizados, cabeceras, cookies y SSL. Siempre establezca CURLOPT_RETURNTRANSFER para obtener la respuesta como cadena (de lo contrario se hace echo directamente). CURLOPT_TIMEOUT previene colgadas en servidores lentos. Para POST con JSON, establezca las cabeceras Content-Type y Content-Length explícitamente. Verifique curl_errno() para errores de conexión y curl_getinfo(CURLINFO_HTTP_CODE) para el estado HTTP. Siempre cierre los handles cURL con curl_close() para liberar recursos. Para código más simple, considere Guzzle (un wrapper de cURL con una API más limpia).
// GET request
$ch = curl_init('https://api.example.com/users');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch)) {
echo 'Error: ' . curl_error($ch);
}
curl_close($ch);
$data = json_decode($response, true);
// POST request with JSON body
$ch = curl_init('https://api.example.com/users');
$payload = json_encode(['name' => 'Alice', 'email' => '[email protected]']);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $token,
'Content-Length: ' . strlen($payload)
],
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
curl_close($ch);cURL con Autenticación y Cookies
cURL soporta múltiples métodos de autenticación. CURLOPT_USERPWD establece HTTP Basic Auth. Los tokens Bearer van en la cabecera Authorization. Para sesiones basadas en cookies (como loguearse en un sitio web), use CURLOPT_COOKIEJAR para guardar cookies y CURLOPT_COOKIEFILE para enviarlas en peticiones subsiguientes — esto mantiene una sesión a través de múltiples llamadas cURL. Use un archivo temporal para cookies y límpielo con unlink(). Para llamadas API, prefiera auth basada en tokens (Bearer) sobre cookies. Siempre use HTTPS (cURL verifica SSL por defecto — no deshabilite CURLOPT_SSL_VERIFYPEER en producción).
// Basic Auth
$ch = curl_init('https://api.example.com/data');
curl_setopt($ch, CURLOPT_USERPWD, 'username:password');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
// Bearer token (API key)
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $apiKey,
'Accept: application/json'
]);
// Cookie-based session (login then reuse cookies)
$cookieFile = tempnam(sys_get_temp_dir(), 'cookie');
// Step 1: Login (saves cookies)
$ch = curl_init('https://example.com/login');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query(['user' => 'alice', 'pass' => 'secret']),
CURLOPT_COOKIEJAR => $cookieFile, // save cookies here
CURLOPT_RETURNTRANSFER => true,
]);
curl_exec($ch);
curl_close($ch);
// Step 2: Access protected page (sends saved cookies)
$ch = curl_init('https://example.com/dashboard');
curl_setopt_array($ch, [
CURLOPT_COOKIEFILE => $cookieFile, // send cookies from here
CURLOPT_RETURNTRANSFER => true,
]);
$dashboard = curl_exec($ch);
curl_close($ch);
unlink($cookieFile); // cleanupDescargas de Archivos y Streaming
Para descargas de archivos grandes, use CURLOPT_FILE para escribir directamente a un file handle — esto evita cargar toda la respuesta en memoria. CURLOPT_FOLLOWLOCATION sigue redirecciones HTTP (301, 302). Para streaming (p. ej., datos en tiempo real), use CURLOPT_WRITEFUNCTION para procesar chunks conforme llegan — útil para APIs que transmiten datos. CURLOPT_PROGRESSFUNCTION monitorea el progreso de descarga/subida. Establezca un CURLOPT_TIMEOUT generoso para archivos grandes. Para subidas muy grandes, use CURLOPT_INFILE para transmitir desde un archivo en lugar de cargar en memoria. Siempre cierre file handles y cURL handles para prevenir fugas de recursos.
// Download a file to disk
$ch = curl_init('https://example.com/large-file.zip');
$fp = fopen('downloaded.zip', 'w');
curl_setopt_array($ch, [
CURLOPT_FILE => $fp, // write directly to file
CURLOPT_FOLLOWLOCATION => true, // follow redirects
CURLOPT_TIMEOUT => 300, // 5 min for large files
]);
curl_exec($ch);
fclose($fp);
curl_close($ch);
// Stream response with callback (progress monitoring)
$ch = curl_init('https://example.com/stream');
curl_setopt_array($ch, [
CURLOPT_WRITEFUNCTION => function($ch, $chunk) {
echo $chunk; // stream to output
ob_flush();
flush();
return strlen($chunk);
},
CURLOPT_PROGRESSFUNCTION => function($ch, $dlTotal, $dlNow, $ulTotal, $ulNow) {
if ($dlTotal > 0) {
printf("
Progress: %.1f%%", ($dlNow / $dlTotal) * 100);
}
return 0;
},
]);
curl_exec($ch);
curl_close($ch);Peticiones Concurrentes (Multi cURL)
curl_multi_exec ejecuta múltiples peticiones HTTP en paralelo — dramáticamente más rápido que peticiones secuenciales cuando necesita datos de múltiples endpoints. El patrón: cree un multi handle, añada handles cURL individuales, ejecute el multi handle en un bucle (curl_multi_exec + curl_multi_select para eficiencia), luego recolecte resultados. Esto es útil para agregar datos de múltiples APIs, prefetching de recursos u operaciones batch. Para concurrencia más avanzada, considere ReactPHP o Amp (frameworks PHP asíncronos). Note que multi-cURL sigue bloqueando el proceso PHP — para async verdadero, use event loops o message queues.
// Fetch multiple URLs in parallel (much faster than sequential)
$urls = [
'https://api.example.com/users',
'https://api.example.com/posts',
'https://api.example.com/comments'
];
$multi = curl_multi_init();
$handles = [];
// Create individual handles
foreach ($urls as $i => $url) {
$handles[$i] = curl_init($url);
curl_setopt($handles[$i], CURLOPT_RETURNTRANSFER, true);
curl_multi_add_handle($multi, $handles[$i]);
}
// Execute all requests concurrently
$active = null;
do {
$status = curl_multi_exec($multi, $active);
if ($active) {
curl_multi_select($multi); // wait for activity
}
} while ($active && $status === CURLM_OK);
// Collect results
$responses = [];
foreach ($handles as $i => $ch) {
$responses[$i] = json_decode(curl_multi_getcontent($ch), true);
curl_multi_remove_handle($multi, $ch);
curl_close($ch);
}
curl_multi_close($multi);
// $responses[0], [1], [2] now contain all three API responsesUsar Guzzle (Cliente HTTP Moderno)
Guzzle es el cliente HTTP estándar para PHP moderno — mucho más limpio que cURL raw. Proporciona una API fluida, objetos request/response compliant con PSR-7, middleware (logging, retry) y peticiones async vía Promises. La opción 'json' auto-encodea el body y establece Content-Type. getAsync/postAsync devuelven Promises para peticiones concurrentes sin la complejidad de multi-cURL. El manejo de excepciones está integrado: RequestException captura errores HTTP (4xx, 5xx). Guzzle es usado por la mayoría de frameworks (el HTTP client de Laravel envuelve Guzzle). Instale vía Composer: composer require guzzlehttp/guzzle.
// Guzzle is the most popular PHP HTTP client (composer require guzzlehttp/guzzle)
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
$client = new Client([
'base_uri' => 'https://api.example.com',
'timeout' => 30,
'headers' => ['Accept' => 'application/json'],
]);
// GET request
$response = $client->get('/users/42');
$body = json_decode($response->getBody(), true);
echo $response->getStatusCode(); // 200
// POST with JSON
$response = $client->post('/users', [
'json' => ['name' => 'Alice', 'email' => '[email protected]'],
'headers' => ['Authorization' => 'Bearer ' . $token],
]);
// Concurrent requests (Promise-based)
$promises = [
'users' => $client->getAsync('/users'),
'posts' => $client->getAsync('/posts'),
];
$results = GuzzleHttp\Promise\Utils::settle($promises)->wait();
// Error handling with try/catch
try {
$response = $client->get('/nonexistent');
} catch (RequestException $e) {
echo $e->getResponse()->getStatusCode(); // 404
}DateTime en Profundidad
Crear y Manipular DateTime
DateTime es la clase robusta de fecha/hora de PHP. DateTimeImmutable es preferido sobre DateTime — devuelve un nuevo objeto al modificar, previniendo bugs de mutación accidental (crítico cuando la misma fecha se usa en múltiples lugares). createFromFormat parsea formatos personalizados. modify() acepta expresiones relativas como '+1 week' o 'last day of next month'. Siempre especifique zonas horarias explícitamente para evitar comportamiento dependiente de la configuración del servidor. Para matemática de fechas (añadir intervalos), use DateInterval ('P1D' = 1 día, 'P2W' = 2 semanas, 'PT2H' = 2 horas) con add()/sub().
$now = new DateTime(); // current time
$now->setTimezone(new DateTimeZone('Asia/Shanghai'));
// From string (many formats supported)
$date = new DateTime('2024-06-15 14:30:00');
$date = new DateTime('first day of next month');
$date = new DateTime('next Friday');
// From format (precise parsing)
$date = DateTime::createFromFormat('d/m/Y', '15/06/2024');
// Modify dates with relative expressions
$date->modify('+1 week');
$date->modify('-2 days');
$date->modify('last day of this month');
$date->modify('+1 year 3 months');
// Immutable version (recommended — doesn't modify original)
$dt = new DateTimeImmutable('2024-06-15');
$next = $dt->modify('+1 day'); // $dt unchanged, $next is new object
echo $dt->format('Y-m-d'); // 2024-06-15 (unchanged!)
echo $next->format('Y-m-d'); // 2024-06-16Formateo y Zonas Horarias
format() usa letras de patrón para personalizar salida — Y (año de 4 dígitos), m (mes de 2 dígitos), d (día de 2 dígitos), H (24 horas), i (minutos), s (segundos). Para ISO 8601 (usado en APIs), use 'Y-m-d\TH:i:sP' o el atajo 'c'. Conversión de zona horaria: cree con la zona horaria origen, luego setTimezone para convertir. Siempre almacene fechas en UTC en la base de datos y convierta a la zona horaria del usuario solo para visualización. La base de datos de zonas horarias de PHP es exhaustiva (incluye reglas DST). Use DateTimeZone::listIdentifiers() para obtener todas las zonas soportadas. El carácter de escape 'T' en format() muestra una 'T' literal (para ISO 8601).
// Format codes (most common):
// Y=2024 y=24 m=06 n=6 d=15 j=15
// H=14 (24h) h=02 (12h) i=30 s=00 A=PM a=pm
// D=Mon l=Monday M=Jun F=June N=1 (Mon=1..Sun=7)
$date = new DateTime('2024-06-15 14:30:00');
echo $date->format('Y-m-d H:i:s'); // 2024-06-15 14:30:00
echo $date->format('l, F j, Y'); // Saturday, June 15, 2024
echo $date->format('Y-m-d\TH:i:sP'); // 2024-06-15T14:30:00+08:00 (ISO 8601)
// Timezone conversion
$utc = new DateTime('2024-06-15 14:00:00', new DateTimeZone('UTC'));
$utc->setTimezone(new DateTimeZone('Asia/Shanghai'));
echo $utc->format('H:i'); // 22:00 (UTC+8)
// List supported timezones
$tzs = DateTimeZone::listIdentifiers();
// ['UTC', 'America/New_York', 'Asia/Shanghai', ...]
// Get timezone offset
$tz = new DateTimeZone('America/Los_Angeles');
$offset = $tz->getOffset(new DateTime()); // seconds from UTCIntervalos de Fecha y Diferencias
DateInterval representa una duración de tiempo usando formato de duración ISO 8601 (P1Y2M3DT4H5M6S). add() y sub() aplican intervalos a fechas. diff() devuelve un DateInterval representando la diferencia entre dos fechas — la propiedad 'days' da días totales, mientras 'y', 'm', 'd' dan desgloses por componente. La propiedad 'invert' indica dirección (1 si la segunda fecha es anterior). Tenga cuidado con la aritmética de meses: añadir 'P1M' a Jan 31 da Mar 2 (Feb tiene 28-29 días), no Feb 31. Para cálculos de días laborables, itere y omita fines de semana/festivos manualmente o use una biblioteca como nesbot/carbon.
// DateInterval: represents a duration
$interval = new DateInterval('P1Y2M3DT4H5M6S');
// P = period, 1Y = 1 year, 2M = 2 months, 3D = 3 days
// T = time separator, 4H = 4 hours, 5M = 5 minutes, 6S = 6 seconds
$date = new DateTime('2024-06-15');
$date->add(new DateInterval('P1M')); // +1 month → 2024-07-15
$date->sub(new DateInterval('P10D')); // -10 days → 2024-07-05
// Difference between two dates
$d1 = new DateTime('2024-01-01');
$d2 = new DateTime('2024-12-31');
$diff = $d1->diff($d2);
echo $diff->days; // 365 (total days)
echo $diff->m; // 0 (months component)
echo $diff->y; // 0 (years component)
echo $diff->format('%a days'); // 365 days
echo $diff->format('%y years, %m months, %d days'); // 0 years, 11 months, 30 days
// Check if inverted (d2 < d1)
echo $diff->invert; // 0 if d2 >= d1, 1 if d2 < d1DatePeriod (Iterar Rangos de Fechas)
DatePeriod itera sobre un rango de fechas a un intervalo especificado — perfecto para generar calendarios, informes o eventos recurrentes. El constructor toma (start, interval, end) o (start, interval, recurrences). La fecha final es exclusiva. Casos de uso comunes: generar todos los días de un mes para una vista de calendario, listar periodos de pago o crear horarios de eventos recurrentes. Use iterator_to_array() para materializar el periodo en un array. Para reglas de recurrencia complejas (p. ej., 'cada 2º martes'), considere una biblioteca dedicada como rrule (reglas de recurrencia RFC 5545).
// DatePeriod iterates over a range of dates
$start = new DateTime('2024-06-01');
$end = new DateTime('2024-06-10');
$interval = new DateInterval('P1D'); // 1 day
$period = new DatePeriod($start, $interval, $end);
foreach ($period as $day) {
echo $day->format('Y-m-d (D)') . "\n";
}
// 2024-06-01 (Sat)
// 2024-06-02 (Sun)
// ... through 2024-06-09 (end is exclusive)
// Generate next 12 months
$start = new DateTime('first day of this month');
$interval = new DateInterval('P1M');
$period = new DatePeriod($start, $interval, 12); // 12 recurrences
foreach ($period as $month) {
echo $month->format('F Y') . "\n";
}
// Every Monday for a year
$start = new DateTime('next Monday');
$end = new DateTime('+1 year');
$period = new DatePeriod($start, new DateInterval('P1W'), $end);
$mondays = iterator_to_array($period);Biblioteca Carbon (DateTime Mejorado)
Carbon extiende DateTime con una API fluida y expresiva — es el estándar de facto en el ecosistema PHP (usado por Laravel). diffForHumans() produce 'hace 5 días', 'en 3 horas' — perfecto para timestamps de UI. La API fluida encadena métodos (addYear()->subMonth()->endOfMonth()). Métodos de comparación (isWeekend, isPast, isToday) simplifican verificaciones comunes. La localización soporta 50+ idiomas para salida legible por humanos. Carbon 3 (2024+) es inmutable por defecto. Instale vía Composer: composer require nesbot/carbon. Si está usando Laravel, Carbon ya está incluido.
// Carbon: the most popular DateTime library (composer require nesbot/carbon)
use Carbon\Carbon;
$now = Carbon::now('Asia/Shanghai');
$tomorrow = Carbon::tomorrow();
$lastWeek = Carbon::now()->subWeek();
// Human-readable differences
echo Carbon::now()->diffForHumans(Carbon::now()->subDays(5));
// "5 days ago"
echo Carbon::now()->addHours(3)->diffForHumans();
// "3 hours from now"
// Fluent API
$date = Carbon::create(2024, 6, 15, 14, 30, 0)
->addYear()
->subMonth()
->endOfMonth()
->setTimezone('UTC');
// Comparison methods
if ($date->isWeekend()) { echo "Weekend!"; }
if ($date->isPast()) { echo "Past"; }
if ($date->isFuture()) { echo "Future"; }
if ($date->isToday()) { echo "Today"; }
if ($date->isLeapYear()) { echo "Leap year"; }
// Localization
Carbon::setLocale('zh');
echo Carbon::now()->subDay()->diffForHumans(); // "1天前"Namespaces y Autoloading
Fundamentos de Namespace
Los namespaces organizan código en paquetes jerárquicos, previniendo colisiones de nombres de clase entre bibliotecas. La declaración namespace debe ser la primera sentencia (después de declare()). La sentencia 'use' importa clases de otros namespaces — coloque las sentencias use al principio del archivo. El aliasing (as) resuelve conflictos cuando dos clases tienen el mismo nombre. El backslash inicial (\DateTime) se refiere al namespace global. Los namespaces de PHP usan backslashes (\) como separadores, mapeando a la estructura de directorios en autoloading PSR-4. Las sentencias group use (use App\Models\{User, Post}) reducen boilerplate.
<?php
// Namespaces prevent name collisions (like packages in Java)
namespace App\Services;
class UserService {
public function find($id) { /* ... */ }
}
// Using namespaced classes
use App\Services\UserService;
use App\Models\User;
$service = new UserService();
$user = new User();
// Aliasing (resolve conflicts or shorten names)
use App\Services\UserService as USvc;
use App\Models\User as UserModel;
// Global namespace (backslash prefix)
$now = new \DateTime(); // \ means root/global namespace
$array = new \ArrayObject();
// Multiple use statements grouped
use App\Models\{User, Post, Comment};
use App\Services\{UserService, PostService};Estándar de Autoloading PSR-4
PSR-4 es la especificación estándar de autoloading — mapea namespaces a rutas de directorio, así que nunca necesita sentencias require/include manuales. La regla: App\Services\UserService mapea a src/Services/UserService.php (App\ → src/). Configure el mapeo en la sección autoload de composer.json. Después de añadir nuevas clases, ejecute 'composer dump-autoload' para regenerar el class map. El archivo vendor/autoload.php (generado por Composer) maneja la carga — inclúyalo una vez en su entry point (index.php). PSR-4 exige que los nombres de clase coincidan con los nombres de archivo (UserService → UserService.php).
// PSR-4: namespace structure maps to file paths
// App\Services\UserService → src/Services/UserService.php
// composer.json PSR-4 configuration:
// {
// "autoload": {
// "psr-4": {
// "App\\": "src/"
// }
// }
// }
// File: src/Services/UserService.php
namespace App\Services;
class UserService {
public function getUser($id) {
return "User $id";
}
}
// File: src/Models/User.php
namespace App\Models;
class User {
public $name;
public function __construct($name) {
$this->name = $name;
}
}
// After adding classes, regenerate autoloader:
// $ composer dump-autoload
// The autoloader is included once in your entry point:
require __DIR__ . '/vendor/autoload.php';
$service = new App\Services\UserService();
$user = new App\Models\User('Alice');Autoloading Sin Composer (spl_autoload)
spl_autoload_register registra una función que se llama cuando una clase aún no está cargada — recibe el nombre de clase fully-qualified y debería requerir el archivo correspondiente. Puede registrar múltiples autoloaders (se llaman en orden). Esto es lo que Composer usa internamente. Para producción, siempre use el autoloader PSR-4 de Composer — está optimizado, maneja casos edge y genera class maps para lookups más rápidos. Use spl_autoload_register directamente solo para proyectos diminutos o cuando Composer no está disponible. El parámetro 'true' en class_exists() dispara autoloading si la clase no está cargada.
<?php
// Simple autoloader for small projects (no Composer needed)
spl_autoload_register(function ($className) {
// Convert namespace separators to directory separators
$file = __DIR__ . '/src/' . str_replace('\\', '/', $className) . '.php';
// App\Services\UserService → src/App/Services/UserService.php
// Adjust prefix mapping as needed:
$file = str_replace('App/', '', $file); // remove 'App' prefix
if (file_exists($file)) {
require $file;
}
});
// Now classes are loaded automatically on first use
$service = new App\Services\UserService();
// Multiple autoloaders (called in order until one loads the class)
spl_autoload_register(function ($class) {
$path = __DIR__ . '/lib/' . $class . '.php';
if (file_exists($path)) require $path;
});
// Check if a class is autoloadable
if (class_exists('App\Helper', true)) { // true = attempt autoload
$helper = new App\Helper();
}Constantes y Funciones de Namespace
Los namespaces pueden contener constantes y funciones, no solo clases. Impórtelas con 'use const' y 'use function' (PHP 5.6+). Esto es útil para constantes de configuración y funciones utilitarias. Las llamadas a función/constante no calificadas tienen un comportamiento de fallback: PHP primero busca en el namespace actual, luego cae al namespace global. Por eso puede llamar strlen() sin un backslash — pero por rendimiento y claridad, prefije funciones globales con \ en código namespaced. Las importaciones group (use App\Config\{const DB_HOST, function connect}) reducen verbosidad.
<?php
namespace App\Config;
// Constants in namespaces
const DB_HOST = 'localhost';
const DB_PORT = 3306;
// Functions in namespaces
function connect() {
return 'Connected to ' . DB_HOST;
}
// Using namespaced constants and functions
use const App\Config\DB_HOST;
use function App\Config\connect;
echo DB_HOST; // localhost
echo connect(); // Connected to localhost
// Or with fully-qualified names:
echo \App\Config\DB_HOST;
echo \App\Config\connect();
// Namespace-level use for multiple imports
use App\Config\{const DB_HOST, const DB_PORT, function connect};
// Fallback: unqualified function calls fall back to global
// if not found in current namespace
namespace App;
$len = strlen('hello'); // calls global strlen() (fallback)Clases Anónimas y Autoloading
Las clases anónimas (PHP 7+) le permiten crear objetos simples de un solo uso sin definir una clase con nombre — útiles para interfaces, mock objects y callbacks. Pueden implementar interfaces, extender clases, tener constructores y usar traits. La clase se genera en runtime con un nombre auto-generado (class@anonymous). Las clases anónimas se cargan inmediatamente (sin autoloading necesario). Úselas para: patrones strategy simples, test doubles/mocks, event listeners y DTOs. Para clases reutilizables, siempre defina clases con nombre con autoloading PSR-4 adecuado. Las clases anónimas son especialmente útiles en tests para crear stubs ligeros.
<?php
namespace App\Factory;
// Anonymous class (PHP 7+): create a one-off class inline
interface Logger {
public function log(string $msg): void;
}
class App {
private $logger;
public function setLogger(Logger $logger) {
$this->logger = $logger;
}
}
$app = new App();
$app->setLogger(new class implements Logger {
public function log(string $msg): void {
echo "[LOG] $msg\n";
}
});
// Anonymous class with constructor
$comparator = new class($ascending = true) {
private $asc;
public function __construct(bool $asc) { $this->asc = $asc; }
public function compare($a, $b): int {
return $this->asc ? $a <=> $b : $b <=> $a;
}
};
// Get the auto-generated class name
echo get_class($comparator); // class@anonymous...OOP en Profundidad (Traits, Interfaces, Abstract)
Clases y Métodos Abstractos
Las clases abstractas proporcionan una base con implementación compartida que las subclases extienden. No pueden instanciarse directamente. Los métodos abstractos definen un contrato (solo firma) que las subclases concretas deben implementar — esto es el 'template method pattern'. A diferencia de las interfaces, las clases abstractas pueden tener propiedades, constructores y métodos concretos. Use clases abstractas cuando las subclases comparten implementación significativa (la relación 'es-un'). Use interfaces cuando solo necesita un contrato que cualquier clase puede implementar (la relación 'puede-hacer'). Una clase puede extender solo una clase abstracta pero implementar múltiples interfaces.
<?php
// Abstract class: can't be instantiated, may have abstract methods
abstract class Animal {
protected $name;
public function __construct(string $name) {
$this->name = $name;
}
// Abstract method: must be implemented by subclasses
abstract public function makeSound(): string;
// Concrete method: shared implementation
public function describe(): string {
return $this->name . " says " . $this->makeSound();
}
}
class Dog extends Animal {
public function makeSound(): string {
return "Woof!";
}
}
class Cat extends Animal {
public function makeSound(): string {
return "Meow!";
}
}
$dog = new Dog("Rex");
echo $dog->describe(); // Rex says Woof!
// new Animal("test"); // Error: cannot instantiate abstract classInterfaces e Implementación Múltiple
Las interfaces definen un contrato — firmas de métodos sin implementación. Una clase puede implementar múltiples interfaces (a diferencia de la herencia simple para clases). Las interfaces habilitan polimorfismo: cualquier clase que implemente Comparable puede ser ordenada, independientemente de su tipo concreto. Use interfaces para definir capacidades (Comparable, Serializable, Iterable) que cruzan jerarquías de clases. El type hinting con interfaces (function sort(Comparable $a)) es más flexible que clases concretas. La herencia de interfaces (interface A extends B, C) combina contratos. PHP moderno también soporta constantes de interfaz y métodos estáticos en interfaces.
<?php
// Interface: pure contract (no implementation)
interface Comparable {
public function compareTo($other): int;
}
interface Serializable {
public function serialize(): string;
public function unserialize(string $data): void;
}
// A class can implement MULTIPLE interfaces
class Product implements Comparable, Serializable {
private $price;
public function __construct(float $price) {
$this->price = $price;
}
public function compareTo($other): int {
return $this->price <=> $other->price; // spaceship operator
}
public function serialize(): string {
return serialize($this->price);
}
public function unserialize(string $data): void {
$this->price = unserialize($data);
}
}
// Type hinting with interfaces
function sortItems(array $items): array {
usort($items, fn($a, $b) => $a->compareTo($b));
return $items;
}
// Interface inheritance
interface Repository extends Comparable, Serializable {
public function find(int $id): ?object;
}Traits (Reutilización de Código Sin Herencia)
Los traits proporcionan reutilización horizontal de código — métodos que pueden 'pegarse' en cualquier clase sin herencia. Esto resuelve el problema del diamante (PHP tiene herencia simple). Usos comunes de traits: logging, patrón singleton, soft deletes, timestamps. Una clase puede usar múltiples traits. Cuando los traits tienen métodos conflictivos, use 'insteadof' para elegir uno y 'as' para alias el otro. Los traits pueden tener métodos abstractos (forzando a la clase que los usa a implementarlos) y métodos/propiedades estáticos. Tenga cuidado de no abusar de los traits — pueden hacer el código más difícil de rastrear. Prefiera composición (inyectar dependencias) sobre traits para comportamiento complejo.
<?php
// Trait: reusable method groups (PHP's answer to multiple inheritance)
trait Logger {
protected function log(string $msg, string $level = 'INFO'): void {
echo "[$level] " . date('Y-m-d H:i:s') . " $msg\n";
// In real code: write to file/database
}
}
trait Singleton {
private static $instance;
public static function getInstance(): self {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
}
class UserService {
use Logger, Singleton;
public function findUser($id) {
$this->log("Finding user $id");
return "User $id";
}
}
$svc = UserService::getInstance();
$svc->findUser(42); // [INFO] 2024-06-15 14:30:00 Finding user 42
// Conflict resolution when traits have same method
trait A { public function hello() { return 'A'; } }
trait B { public function hello() { return 'B'; } }
class C {
use A, B {
B::hello insteadof A; // use B's hello
A::hello as helloFromA; // alias A's as helloFromA
}
}Late Static Binding (static:: vs self::)
Late Static Binding (LSB) es la diferencia entre self:: (tiempo de compilación, siempre se refiere a la clase definitoria) y static:: (runtime, se refiere a la clase llamadora). Esto importa en herencia: si Base tiene un método usando self::$table, siempre ve el $table de Base incluso cuando se llama en Child. Usar static::$table hace que vea el $table de Child. LSB es esencial para patrones factory (new static() crea instancias de la clase llamadora), ActiveRecord (cada modelo tiene su propia tabla) y el patrón singleton. El tipo de retorno 'static' (PHP 8+) declara que el método devuelve una instancia de la clase llamadora.
<?php
class Base {
protected static $table = 'base';
public static function getTable(): string {
// self:: refers to the class where the method is DEFINED
return self::$table; // always 'base'
}
public static function getTableStatic(): string {
// static:: refers to the class that was CALLED (runtime)
return static::$table; // late static binding
}
public static function create(): static {
// 'static' return type + new static() = factory pattern
return new static();
}
}
class Child extends Base {
protected static $table = 'child';
}
echo Child::getTable(); // 'base' (self:: = Base)
echo Child::getTableStatic(); // 'child' (static:: = Child)
echo get_class(Child::create()); // 'Child' (new static = Child)
// self:: is resolved at compile time (the defining class)
// static:: is resolved at runtime (the calling class)
// This is "Late Static Binding" — essential for factory patternsMétodos Mágicos
Los métodos mágicos son métodos especiales que interceptan operaciones de objeto. __get/__set crean propiedades dinámicas (útiles para objetos de transferencia de datos, ORMs). __toString habilita echo $object. __invoke hace un objeto invocable como una función. __isset/__unset soportan isset()/unset() en propiedades dinámicas. __debugInfo personaliza la salida de var_dump. Otros métodos mágicos: __construct, __destruct, __clone (para clonación profunda), __call/__callStatic (para métodos no definidos, habilita APIs fluidos y mixins), __serialize/__unserialize (reemplaza __sleep/__wakeup en PHP 7.4+). Use métodos mágicos con moderación — añaden comportamiento 'mágico' que puede ser difícil de depurar. Documentelos claramente.
<?php
class MagicBox {
private $data = [];
// Called when accessing undefined properties
public function __get($name) {
return $this->data[$name] ?? null;
}
// Called when setting undefined properties
public function __set($name, $value) {
$this->data[$name] = $value;
}
// Called when isset() or empty() on undefined property
public function __isset($name): bool {
return isset($this->data[$name]);
}
// Called when unset() on undefined property
public function __unset($name): void {
unset($this->data[$name]);
}
// Called when object is used as string
public function __toString(): string {
return json_encode($this->data);
}
// Called when object is called as function
public function __invoke($arg) {
return "Called with $arg";
}
// Called for var_dump/debugging
public function __debugInfo(): array {
return ['keys' => array_keys($this->data)];
}
}
$box = new MagicBox();
$box->name = "Alice"; // __set
echo $box->name; // __get → Alice
echo $box; // __toString → {"name":"Alice"}
echo $box("test"); // __invoke → Called with testGestión de Paquetes Composer
Fundamentos de composer.json
composer.json es el manifiesto para proyectos PHP. require lista dependencias de producción con restricciones de versión (^ permite actualizaciones minor, ~ permite patch). autoload define el mapeo namespace-a-directorio PSR-4. require-dev contiene dependencias solo de desarrollo. Ejecute composer install para configurar el proyecto.
{
"name": "myorg/myapp",
"type": "project",
"require": {
"php": ">=8.1",
"monolog/monolog": "^3.0",
"symfony/console": "^7.0"
},
"require-dev": {
"phpunit/phpunit": "^11.0"
},
"autoload": {
"psr-4": { "MyApp\\": "src/" }
}
}Instalar y Actualizar
composer install lee composer.lock para versiones exactas (builds reproducibles). composer require añade un paquete y resuelve dependencias. composer update obtiene versiones más nuevas dentro de las restricciones. Use --no-dev para producción. --optimize-autoloader convierte PSR-4 a classmap para autoloading más rápido en producción.
# Install all dependencies from composer.lock
composer install
# Add a package (modifies composer.json)
composer require monolog/monolog
composer require --dev phpunit/phpunit
# Update all packages to latest allowed versions
composer update
# Update a single package
composer update monolog/monolog
# Production install (no dev, optimized autoloader)
composer install --no-dev --optimize-autoloader
# Show installed packages
composer showRestricciones de Versión
El caret (^) es la restricción más común: permite cambios que no modifican el dígito no-cero más a la izquierda. La tilde (~) bloquea a nivel de patch. Para versiones 0.x, ^0.3 permite 0.3.x pero no 0.4. Siempre use restricciones para obtener parches de seguridad mientras evita cambios disruptivos. Fije versiones exactas en composer.lock para reproducibilidad.
"require": {
// Caret: >=1.2.0, <2.0.0 (allows minor+patch)
"vendor/pkg": "^1.2",
// Tilde: >=1.2.0, <1.3.0 (patch only)
"vendor/pkg2": "~1.2",
// Exact version
"vendor/pkg3": "1.2.3",
// Range
"vendor/pkg4": ">=1.0 <2.0",
// Wildcard
"vendor/pkg5": "1.2.*",
// Stability flags
"vendor/pkg6": "dev-main",
"vendor/pkg7": "2.0@beta"
}Autoloading PSR-4
El autoloading PSR-4 mapea prefijos de namespace a directorios: MyApp\Services\UserService resuelve a src/Services/User.php. Ejecute composer dump-autoload después de añadir nuevas clases. Para producción, use --optimize para generar un classmap (una lookup de array en lugar de verificaciones de sistema de archivos). El autoloading classmap escanea directorios y es el más rápido para codebases fijos.
// PSR-4: "MyApp\\": "src/" means
// MyApp\User -> src/User.php
// MyApp\Services\Auth -> src/Services/Auth.php
namespace MyApp\Services;
class UserService
{
public function find(int $id): ?User
{
// ...
}
}
// After running composer dump-autoload:
// require 'vendor/autoload.php';
// $service = new \MyApp\Services\UserService();
// Classmap (faster for production)
// "autoload": { "classmap": ["src/", "lib/"] }Scripts y Hooks
Los scripts de Composer definen comandos específicos del proyecto. Ejecútelos con composer <nombre>. Los eventos integrados (post-install-cmd, post-update-cmd, pre-autoload-dump) se disparan automáticamente. Los scripts pueden referenciar otros scripts con @name. Use scripts para estandarizar flujos de trabajo de desarrollo entre miembros del equipo.
{
"scripts": {
"test": "phpunit",
"lint": "phpcs --standard=PSR12 src/",
"fix": "phpcbf --standard=PSR12 src/",
"post-install-cmd": [
"MyApp\\Setup::postInstall",
"php artisan migrate"
],
"post-update-cmd": "@post-install-cmd"
}
}
// Run: composer test, composer lint, etc.cURL Avanzado
Multi-Request (Paralelo)
curl_multi_exec ejecuta múltiples peticiones en paralelo, reduciendo dramáticamente el tiempo total para llamadas API batch. curl_multi_select bloquea hasta que hay actividad, evitando busy-waiting. Siempre cierre handles y el multi handle para liberar recursos. Esta es la fundación del scraping HTTP de alto rendimiento y agregación de APIs.
<?php
$urls = [
'https://api.example.com/users',
'https://api.example.com/posts',
'https://api.example.com/comments',
];
$multi = curl_multi_init();
$handles = [];
foreach ($urls as $i => $url) {
$handles[$i] = curl_init($url);
curl_setopt($handles[$i], CURLOPT_RETURNTRANSFER, true);
curl_setopt($handles[$i], CURLOPT_TIMEOUT, 10);
curl_multi_add_handle($multi, $handles[$i]);
}
do {
$status = curl_multi_exec($multi, $active);
if ($active) curl_multi_select($multi);
} while ($active && $status === CURLM_OK);
foreach ($handles as $i => $ch) {
$responses[$i] = curl_multi_getcontent($ch);
curl_multi_remove_handle($multi, $ch);
}
curl_multi_close($multi);Respuestas Streaming
CURLOPT_WRITEFUNCTION proporciona un callback para cada chunk de la respuesta, habilitando procesamiento streaming de archivos grandes sin cargarlos completamente en memoria. Devuelva la longitud del chunk para señalar consumo. Esto es esencial para descargar archivos grandes, procesar APIs streaming o parsear CSV/JSON incrementalmente.
<?php
$ch = curl_init('https://example.com/large-file.csv');
$file = fopen('download.csv', 'w');
curl_setopt($ch, CURLOPT_FILE, $file); // Write to file
// OR use callback for streaming processing:
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $chunk) use ($file) {
fwrite($file, $chunk);
// Or parse incrementally:
// $lines = explode("\n", $chunk);
return strlen($chunk); // Must return bytes consumed
});
curl_exec($ch);
fclose($file);
curl_close($ch);Autenticación y Cookies
Establezca cabeceras personalizadas con CURLOPT_HTTPHEADER para autenticación (Bearer tokens, claves de API). COOKIEJAR/COOKIEFILE persisten cookies entre peticiones para auth basada en sesión. CURLOPT_USERPWD establece HTTP Basic Auth. Para POST, establezca CURLOPT_POSTFIELDS con JSON y la cabecera Content-Type. Siempre establezca Accept para controlar el formato de respuesta.
<?php
$ch = curl_init('https://api.example.com/data');
// Bearer token
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $token,
'Content-Type: application/json',
]);
// Cookie jar (persist across requests)
curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt'); // Save
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt'); // Load
// Basic auth
curl_setopt($ch, CURLOPT_USERPWD, 'username:password');
// POST with JSON body
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['key' => 'value']));
$response = curl_exec($ch);Manejo de Errores y Reintentos
Siempre verifique el valor de retorno de curl_exec (false en fallo) y curl_error para el mensaje. curl_getinfo proporciona el código de estado HTTP, timing e información de redirección. Implemente backoff exponencial para reintentos para manejar rate limits y fallos transitorios. Distinga entre errores de red (error de curl) y errores HTTP (código de estado).
<?php
function fetchWithRetry(string $url, int $max = 3): ?string
{
for ($attempt = 1; $attempt <= $max; $attempt++) {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch);
$error = curl_error($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($response !== false && $code >= 200 && $code < 300) {
return $response;
}
if ($attempt < $max) {
sleep(pow(2, $attempt)); // Exponential backoff
}
}
throw new RuntimeException("Failed: {$error}");
}Referencia de Opciones cURL
CURLOPT_FOLLOWLOCATION sigue redirecciones HTTP (3xx). Siempre mantenga SSL_VERIFYPEER true en producción para prevenir ataques MITM; descargue cacert.pem de curl.haxx.se. CURLOPT_ENCODING habilita compresión. Use CURLOPT_VERBOSE con STDERR para depurar problemas de conexión. Establezca timeouts razonables para evitar colgadas.
<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://example.com');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // Return not echo
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); // Follow redirects
curl_setopt($ch, CURLOPT_MAXREDIRS, 5);
// SSL (keep VERIFYPEER true in production!)
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_CAINFO, '/path/to/cacert.pem');
// Performance
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_ENCODING, 'gzip');
// Debug
curl_setopt($ch, CURLOPT_VERBOSE, true);
curl_setopt($ch, CURLOPT_STDERR, fopen('curl.log', 'w'));Procesamiento de Imágenes (GD)
Crear y Cargar Imágenes
imagecreatetruecolor crea una imagen true color (millones de colores). imagecolorallocate registra un color y devuelve un identificador. imagecreatefromjpeg/png/webp carga archivos existentes. Siempre verifique el valor de retorno (false en fallo). Use imagesx/imagesy para obtener dimensiones. Libere memoria con imagedestroy cuando termine.
<?php
// Create a blank image
$img = imagecreatetruecolor(400, 300);
// Allocate colors
$white = imagecolorallocate($img, 255, 255, 255);
$red = imagecolorallocate($img, 255, 0, 0);
// Fill background
imagefill($img, 0, 0, $white);
// Load existing images
$photo = imagecreatefromjpeg('photo.jpg');
$png = imagecreatefrompng('logo.png');
$webp = imagecreatefromwebp('image.webp');
// Get dimensions
$width = imagesx($photo);
$height = imagesy($photo);Dibujar Formas y Texto
GD proporciona primitivas de dibujo: rectángulos, elipses, líneas, polígonos y arcos. Las variantes filled (imagefilled*) dibujan formas sólidas. imagettftext renderiza fuentes TrueType con control de ángulo y tamaño. Siempre envíe una cabecera Content-Type antes de mostrar datos de imagen. Llame imagedestroy para liberar memoria.
<?php
$img = imagecreatetruecolor(400, 300);
$white = imagecolorallocate($img, 255, 255, 255);
$red = imagecolorallocate($img, 255, 0, 0);
$blue = imagecolorallocate($img, 0, 0, 255);
imagefill($img, 0, 0, $white);
// Shapes
imagerectangle($img, 50, 50, 150, 120, $red);
imagefilledrectangle($img, 200, 50, 300, 120, $blue);
imageellipse($img, 100, 200, 80, 80, $red);
imageline($img, 0, 0, 400, 300, $red);
// Text with TrueType font
imagettftext($img, 20, 0, 50, 270, $red, 'arial.ttf', 'Hello GD!');
header('Content-Type: image/png');
imagepng($img);
imagedestroy($img);Redimensionar y Recortar
imagecopyresampled produce resultados de mayor calidad que imagecopyresized (usa interpolación). Mantenga la relación de aspecto calculando dimensiones desde el original. Para miniaturas, center-crop a un cuadrado para layout consistente. Siempre destruya las imágenes fuente después de copiar para prevenir fugas de memoria en procesamiento batch.
<?php
function resizeImage($src, $maxW, $maxH) {
list($w, $h) = getimagesize($src);
$ratio = min($maxW / $w, $maxH / $h);
$newW = (int)($w * $ratio);
$newH = (int)($h * $ratio);
$srcImg = imagecreatefromjpeg($src);
$dstImg = imagecreatetruecolor($newW, $newH);
// High-quality resampling
imagecopyresampled($dstImg, $srcImg, 0, 0, 0, 0,
$newW, $newH, $w, $h);
imagedestroy($srcImg);
return $dstImg;
}
// Center crop to square
function cropSquare($src, $size) {
list($w, $h) = getimagesize($src);
$min = min($w, $h);
$x = (int)(($w - $min) / 2);
$y = (int)(($h - $min) / 2);
$dst = imagecreatetruecolor($size, $size);
$img = imagecreatefromjpeg($src);
imagecopyresampled($dst, $img, 0, 0, $x, $y, $size, $size, $min, $min);
return $dst;
}Filtros y Efectos
imagefilter aplica efectos integrados: grayscale, brightness (rango -255 a 255), contrast (negativo aumenta), blur, edge detection, negate y colorize (RGB + alpha). Pixelate crea un efecto mosaico. Estos son rápidos pero básicos; para efectos avanzados, use ImageMagick (extensión Imagick) que soporta matrices de convolución y filtros personalizados.
<?php
$img = imagecreatefromjpeg('photo.jpg');
// Built-in filters
imagefilter($img, IMG_FILTER_GRAYSCALE); // B&W
imagefilter($img, IMG_FILTER_BRIGHTNESS, 30); // Brighten
imagefilter($img, IMG_FILTER_CONTRAST, -20); // More contrast
imagefilter($img, IMG_FILTER_GAUSSIAN_BLUR); // Blur
imagefilter($img, IMG_FILTER_EDGEDETECT); // Edge detection
imagefilter($img, IMG_FILTER_NEGATE); // Invert colors
// Colorize (tint)
imagefilter($img, IMG_FILTER_COLORIZE, 0, 0, 100, 0); // Blue tint
// Pixelate
imagefilter($img, IMG_FILTER_PIXELATE, 10, true);
imagepng($img, 'filtered.png');
imagedestroy($img);Marcas de Agua y Composición
imagecopymerge superpone una imagen sobre otra con opacidad ajustable (0-100). Las marcas de agua PNG con canales alpha se mezclan naturalmente. Para marcas de agua de texto, use imagecolorallocatealpha para texto semi-transparente. La calidad de imagejpeg va de 0 (peor) a 100 (mejor); 75-90 es un buen balance para web. Siempre destruya ambas imágenes.
<?php
$photo = imagecreatefromjpeg('photo.jpg');
$watermark = imagecreatefrompng('logo.png');
$pw = imagesx($photo); $ph = imagesy($photo);
$ww = imagesx($watermark); $wh = imagesy($watermark);
// Position: bottom-right with 20px padding
$destX = $pw - $ww - 20;
$destY = $ph - $wh - 20;
// Merge with 50% opacity
imagecopymerge($photo, $watermark, $destX, $destY, 0, 0, $ww, $wh, 50);
// Text watermark
$color = imagecolorallocatealpha($photo, 255, 255, 255, 60);
imagettftext($photo, 30, 0, 20, $ph - 20, $color, 'arial.ttf', '© 2025');
imagejpeg($photo, 'watermarked.jpg', 90); // 90% quality
imagedestroy($photo);
imagedestroy($watermark);REST API Profundo
Routing y Manejo de Peticiones
Las APIs REST mapean métodos HTTP a operaciones CRUD: GET (leer), POST (crear), PUT/PATCH (actualizar), DELETE (eliminar). Parse la ruta URL para identificación de recursos. Lea el body de la petición de php://input para POST/PUT. Siempre devuelva códigos de estado HTTP apropiados (200, 201, 400, 404, 500) y respuestas JSON con cabecera Content-Type.
<?php
$method = $_SERVER['REQUEST_METHOD'];
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$segments = explode('/', trim($path, '/'));
switch (true) {
case $method === 'GET' && $segments[0] === 'users':
if (isset($segments[1])) getUser((int)$segments[1]);
else listUsers();
break;
case $method === 'POST' && $segments[0] === 'users':
$data = json_decode(file_get_contents('php://input'), true);
createUser($data);
break;
case $method === 'PUT' && $segments[0] === 'users' && isset($segments[1]):
$data = json_decode(file_get_contents('php://input'), true);
updateUser((int)$segments[1], $data);
break;
default:
http_response_code(404);
echo json_encode(['error' => 'Not Found']);
}Respuesta y Códigos de Estado
Siempre establezca Content-Type: application/json para respuestas API. Use códigos de estado correctos: 200 (OK), 201 (Created), 204 (No Content), 400 (Bad Request), 401 (Unauthorized), 403 (Forbidden), 404 (Not Found), 422 (Unprocessable Entity), 429 (Too Many Requests), 500 (Server Error). Incluya detalles de error para depuración pero nunca exponga stack traces en producción.
<?php
function jsonResponse($data, int $status = 200): void
{
http_response_code($status);
header('Content-Type: application/json');
echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
exit;
}
// Success responses
jsonResponse(['data' => $users], 200); // OK
jsonResponse(['data' => $user], 201); // Created
// Error responses
jsonResponse(['error' => 'Validation failed'], 400);
jsonResponse(['error' => 'Unauthorized'], 401);
jsonResponse(['error' => 'Forbidden'], 403);
jsonResponse(['error' => 'Not Found'], 404);
jsonResponse(['error' => 'Server error'], 500);Paginación y Filtrado
Implemente paginación con LIMIT/OFFSET y devuelva metadatos (total, página actual, total de páginas). Valide y sanitize columnas de ordenación contra una whitelist para prevenir inyección SQL. Limite per_page para prevenir consultas excesivas. Use LIKE para búsqueda con wildcards. Devuelva metadatos de paginación en un objeto meta separado de los datos.
<?php
$page = max(1, (int)($_GET['page'] ?? 1));
$perPage = min(100, max(1, (int)($_GET['per_page'] ?? 20)));
$offset = ($page - 1) * $perPage;
// Whitelist sort columns (prevent SQL injection)
$sort = in_array($_GET['sort'] ?? '', ['name', 'email'])
? $_GET['sort'] : 'id';
$order = strtolower($_GET['order'] ?? 'asc') === 'desc' ? 'DESC' : 'ASC';
$sql = "SELECT * FROM users ORDER BY $sort $order LIMIT ? OFFSET ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$perPage, $offset]);
$users = $stmt->fetchAll();
// Total count for pagination metadata
$total = (int)$pdo->query("SELECT COUNT(*) FROM users")->fetchColumn();
jsonResponse([
'data' => $users,
'meta' => [
'page' => $page,
'per_page' => $perPage,
'total' => $total,
'total_pages' => ceil($total / $perPage),
],
]);Rate Limiting
El rate limiting previene abuso de APIs. Use algoritmos de ventana fija (simple) o ventana deslizante (más preciso). Almacene contadores en Redis para sistemas distribuidos. Devuelva cabeceras X-RateLimit (Limit, Remaining, Reset) para que los clientes puedan auto-regularse. HTTP 429 con Retry-After indica a los clientes cuándo reintentar. Para producción, use Redis o un rate limiter dedicado.
<?php
function checkRateLimit(int $userId, int $max = 100, int $window = 3600): bool
{
$file = sys_get_temp_dir() . "/rate_{$userId}_" . floor(time() / $window);
$count = file_exists($file) ? (int)file_get_contents($file) : 0;
if ($count >= $max) {
$reset = (floor(time() / $window) + 1) * $window;
header("X-RateLimit-Limit: $max");
header("X-RateLimit-Remaining: 0");
header("Retry-After: " . ($reset - time()));
http_response_code(429);
echo json_encode(['error' => 'Rate limit exceeded']);
return false;
}
file_put_contents($file, $count + 1);
header("X-RateLimit-Remaining: " . ($max - $count - 1));
return true;
}
if (!checkRateLimit($userId)) exit;Versionado de API
Estrategias de versionado de API: prefijo URL (/v1/) es el más explícito y cache-friendly; cabecera Accept es RESTful pero más difícil de testear. Documente APIs con anotaciones OpenAPI (Swagger). Genere docs interactivas con herramientas como swagger-php. Versione desde el inicio; los cambios disruptivos requieren una nueva versión. Depreque versiones antiguas con cabecera Sunset.
<?php
// Versioning via URL prefix: /v1/users, /v2/users
$version = $segments[0] ?? 'v1';
// Versioning via Accept header
// Accept: application/vnd.myapp.v2+json
preg_match('/vnd\.myapp\.(v\d+)\+json/',
$_SERVER['HTTP_ACCEPT'] ?? '', $m);
$version = $m[1] ?? 'v1';
// OpenAPI/Swagger documentation
/**
* @OA\Get(path="/api/users",
* @OA\Response(response=200, description="List users")
* )
*/Seguridad Profunda (XSS/CSRF)
Prevención XSS
XSS (Cross-Site Scripting) inyecta scripts maliciosos en páginas web. Prevengalo codificando la salida según el contexto: htmlspecialchars para HTML, json_encode para JavaScript, urlencode para URLs. ENT_QUOTES escapa tanto comillas simples como dobles. Content-Security-Policy (CSP) añade defensa en profundidad restringiendo fuentes de scripts. Nunca confíe en entrada de usuario.
<?php
// Output encoding (prevents XSS)
echo htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8');
// JavaScript string (use json_encode)
echo '<script>var name = ' . json_encode($name) . ';</script>';
// URL parameter
echo 'redirect=' . urlencode($url);
// Content-Security-Policy header
header("Content-Security-Policy: default-src 'self'; script-src 'self'");
// Disable inline scripts
header("X-XSS-Protection: 1; mode=block");Protección CSRF
CSRF (Cross-Site Request Forgery) engaña a usuarios para enviar acciones no deseadas. Prevengalo con tokens anti-CSRF: genere un token aleatorio por sesión, inclúyalo en formularios como campo oculto y verifique en POST/PUT/DELETE. Use hash_equals para comparación timing-safe. Para AJAX, envíe el token en una cabecera personalizada. Las cookies SameSite=Strict proporcionan protección adicional.
<?php
// Generate CSRF token
function csrfToken(): string {
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
// Verify CSRF token (timing-safe)
function verifyCsrf(string $token): bool {
return !empty($_SESSION['csrf_token'])
&& hash_equals($_SESSION['csrf_token'], $token);
}
// In the form
echo '<input type="hidden" name="csrf_token" value="' . csrfToken() . '">';
// On POST request
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!verifyCsrf($_POST['csrf_token'] ?? '')) {
http_response_code(403);
die('CSRF token validation failed');
}
}Prevención de Inyección SQL
La inyección SQL permite a atacantes ejecutar SQL arbitrario. Siempre use sentencias preparadas con consultas parametrizadas: la base de datos separa la lógica SQL de los datos, haciendo la inyección imposible. Nunca concatene entrada de usuario en cadenas SQL. Para consultas dinámicas (cláusulas IN, ORDER BY), construya la estructura SQL con placeholders y pase valores como parámetros.
<?php
// BAD: string concatenation (SQL injection vulnerable)
$sql = "SELECT * FROM users WHERE name = '" . $_GET['name'] . "'";
// Attack: ?name=' OR '1'='1
// GOOD: prepared statements
$stmt = $pdo->prepare('SELECT * FROM users WHERE name = ? AND status = ?');
$stmt->execute([$_GET['name'], 'active']);
$users = $stmt->fetchAll();
// Named parameters
$stmt = $pdo->prepare(
'INSERT INTO users (name, email) VALUES (:name, :email)'
);
$stmt->execute([':name' => $name, ':email' => $email]);
// Dynamic IN clause (still safe)
$ids = [1, 2, 3];
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($placeholders)");
$stmt->execute($ids);Hashing de Contraseñas
Nunca almacene contraseñas en texto plano. password_hash usa bcrypt (o Argon2 si disponible) con generación automática de salt. El hash incluye el algoritmo, coste y salt, así que password_verify puede verificar contra cualquier formato. Use password_needs_rehash para actualizar hashes cuando aumente el factor de coste o cambie de algoritmos. Argon2 es recomendado para nuevas aplicaciones.
<?php
// Hash a password (uses bcrypt by default, auto-salts)
$hash = password_hash('myPassword123', PASSWORD_DEFAULT);
// $2y$10$... (bcrypt with cost 10)
// Verify a password
if (password_verify($inputPassword, $storedHash)) {
// Check if rehash is needed (algorithm upgrade)
if (password_needs_rehash($storedHash, PASSWORD_DEFAULT)) {
$newHash = password_hash($inputPassword, PASSWORD_DEFAULT);
// Update stored hash in database
}
// Log user in
}
// Argon2 (PHP 7.2+, requires libsodium)
$hash = password_hash('password', PASSWORD_ARGON2ID, [
'memory_cost' => 65536, // 64 MB
'time_cost' => 4, // iterations
'threads' => 2,
]);Validación de Entrada
Valide la entrada del lado del servidor siempre (la validación del lado del cliente es solo para UX). Use filter_input con FILTER_VALIDATE_* para verificación de tipo y FILTER_SANITIZE_* para limpieza. Para reglas personalizadas, use regex o bibliotecas de validación dedicadas (Respect/Validation, Symfony Validator). Use un enfoque whitelist: solo acepte campos conocidos, rechace todo lo demás.
<?php
// Validate email
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
if ($email === false) {
$errors[] = 'Invalid email';
}
// Validate integer with range
$age = filter_input(INPUT_POST, 'age', FILTER_VALIDATE_INT, [
'options' => ['min_range' => 1, 'max_range' => 150]
]);
// Validate URL
$url = filter_input(INPUT_POST, 'website', FILTER_VALIDATE_URL);
// Custom validation with regex
function validateUsername(string $u): ?string {
if (!preg_match('/^[a-zA-Z0-9_]{3,20}$/', $u)) {
return 'Username must be 3-20 alphanumeric chars';
}
return null;
}
// Whitelist approach for arrays
$allowed = ['name', 'email', 'age'];
$input = array_intersect_key($_POST, array_flip($allowed));Namespaces y Autoloading Profundo
Declaración de Namespace
Los namespaces previenen colisiones de nombres de clase y organizan código jerárquicamente. La declaración namespace debe ser la primera sentencia. use importa clases, funciones y constantes. Los aliases (as) resuelven conflictos. El namespacing de PHP usa backslashes. El estándar PSR-4 mapea separadores de namespace a separadores de directorio: MyApp\Services\UserService -> src/Services/UserService.php.
<?php
// File: src/Services/UserService.php
namespace MyApp\Services;
use MyApp\Models\User;
use MyApp\Repositories\UserRepository;
use MyApp\Exceptions\NotFoundException;
class UserService
{
public function __construct(
private UserRepository $repo
) {}
public function find(int $id): User
{
$user = $this->repo->findById($id);
if (!$user) {
throw new NotFoundException("User {$id} not found");
}
return $user;
}
}Autoloading PSR-4
PSR-4 es la especificación estándar de autoloading: un prefijo de namespace mapea a un directorio base, y cada separador de namespace se convierte en un separador de directorio. Composer genera el autoloader que resuelve nombres de clase a rutas de archivo automáticamente. Ejecute composer dump-autoload después de añadir nuevas clases. El autoloader solo carga clases cuando se referencian por primera vez (lazy loading).
// composer.json
{
"autoload": {
"psr-4": {
"MyApp\\": "src/",
"Tests\\": "tests/"
}
}
}
// After composer install:
require __DIR__ . '/vendor/autoload.php';
// Now all classes auto-load:
use MyApp\Services\UserService;
use MyApp\Controllers\UserController;
// PSR-4 rules:
// MyApp\User -> src/User.php
// MyApp\Services\Auth -> src/Services/Auth.php
// MyApp\Models\User\Profile -> src/Models/User/Profile.phpAutoloader Personalizado
spl_autoload_register añade una función a la pila del autoloader. Cuando una clase se referencia pero no está cargada, PHP llama a cada autoloader registrado en orden hasta que uno cargue la clase. Múltiples autoloaders pueden coexistir (p. ej., uno para PSR-4, uno para clases legacy). Siempre verifique si el archivo existe antes de requerir para evitar errores. Composer usa este mecanismo internamente.
<?php
spl_autoload_register(function (string $class): void {
$prefix = 'MyApp\\';
$baseDir = __DIR__ . '/src/';
// Check if class uses our namespace prefix
$len = strlen($prefix);
if (strncmp($prefix, $class, $len) !== 0) {
return; // Not our class
}
// Get relative class name
$relativeClass = substr($class, $len);
// Replace namespace separators with directory separators
$file = $baseDir . str_replace('\\', '/', $relativeClass) . '.php';
if (file_exists($file)) {
require $file;
}
});
// Multiple autoloaders can be registered (stack-based)
spl_autoload_register(function ($class) {
$path = __DIR__ . '/legacy/' . $class . '.php';
if (file_exists($path)) require $path;
});Autoloading Classmap y Files
El autoloading classmap escanea directorios en tiempo de dump-autoload y construye un array mapeando nombres de clase a rutas de archivo. Esto es más rápido que PSR-4 (una lookup de array vs verificaciones de sistema de archivos) y se recomienda para producción. files autoloads archivos específicos en cada petición, útil para funciones helper y constantes que no pueden autoloadearse como clases. Use --optimize para classmap de producción.
{
"autoload": {
"psr-4": { "MyApp\\": "src/" },
"classmap": ["src/Legacy/", "lib/OldClasses.php"],
"files": ["src/helpers.php", "src/constants.php"]
}
}
// classmap: scans directories and builds class-to-file map
// Faster than PSR-4 for production (no filesystem checks)
// files: always-loaded files (for functions and constants)
// src/helpers.php:
<?php
function dd($var) { var_dump($var); die; }
function env(string $key, $default = null) { /* ... */ }Resolución de Namespace
En código namespaced, los nombres de clase no calificados se resuelven a través de imports primero, luego el namespace actual. Las clases integradas (DateTime, PDO, Exception) viven en el namespace global; referéncielas con un backslash inicial o impórtelas. Las funciones y constantes caen al namespace global si no se encuentran localmente. Use FQCN (backslash inicial) para referencias absolutas.
<?php
namespace MyApp\Services;
use MyApp\Models\User;
class UserService
{
public function create(): User
{
// User resolves to MyApp\Models\User (imported)
return new User();
}
public function find(): \MyApp\Models\User
{
// Fully Qualified Class Name (leading backslash)
return new \MyApp\Models\User();
}
public function date(): \DateTime
{
// Global classes need backslash or import
return new \DateTime();
}
}Generadores y Yield
Generador Básico
Los generadores producen valores perezosamente con yield, uno a la vez, sin construir toda la colección en memoria. Esto es eficiente en memoria para secuencias grandes o infinitas. La función devuelve un objeto Generator que implementa Iterator. Cada yield pausa la ejecución, se reanuda en la siguiente iteración. Use generadores para leer archivos grandes, cursores de base de datos y secuencias computadas.
<?php
function rangeGen(int $start, int $end, int $step = 1): Generator
{
for ($i = $start; $i <= $end; $i += $step) {
yield $i;
}
}
// Iterate without loading all values into memory
foreach (rangeGen(1, 5) as $value) {
echo $value . ' '; // 1 2 3 4 5
}
// Memory efficient for large sequences
foreach (rangeGen(1, 1000000) as $value) {
if ($value % 100000 === 0) echo "Reached {$value}\n";
}Yield Pares Key-Value
Los generadores pueden devolver pares key-value usando la sintaxis yield key => value, como arrays asociativos. Esto preserva claves a través de transformaciones. Para filtrar valores, simplemente no los devuelva con yield. El Generator mantiene su posición en la iteración, así que puede construir procesamiento estilo pipeline donde cada generador transforma o filtra el flujo.
<?php
function mapGen(array $data): Generator
{
foreach ($data as $key => $value) {
yield $key => strtoupper($value);
}
}
foreach (mapGen(['a' => 'hello', 'b' => 'world']) as $key => $value) {
echo "{$key} => {$value}\n";
}
// a => HELLO
// b => WORLD
// Filter by not yielding unwanted values
function filterGen(array $data): Generator
{
foreach ($data as $value) {
if ($value > 0) yield $value;
}
}Enviar Valores a Generadores
El método send() pasa un valor al generador, que se convierte en el resultado de la expresión yield. Esto habilita comunicación bidireccional, útil para coroutines y máquinas de estado. current() inicia el generador. getReturn() recupera el valor de retorno después de que el generador completa. El bloque finally se ejecuta cuando el generador se destruye, habilitando limpieza de recursos.
<?php
function accumulator(): Generator
{
$total = 0;
while (true) {
$value = yield $total;
if ($value === null) break;
$total += $value;
}
return $total;
}
$gen = accumulator();
$gen->current(); // Start: 0
$gen->send(10); // Returns 10
$gen->send(20); // Returns 30
$gen->send(5); // Returns 35
$gen->send(null); // Triggers return
echo $gen->getReturn(); // 35Yield from (Delegación)
yield from delega a otro generador, array o Traversable, aplanando sus valores en el generador externo. El valor de retorno del generador interno está disponible para el generador externo. Esto habilita composición: construya pipelines complejos desde generadores simples. yield from también es más eficiente que iterar y re-yield manualmente.
<?php
function innerGen(): Generator
{
yield 1;
yield 2;
return 'inner done';
}
function outerGen(): Generator
{
yield 0;
$result = yield from innerGen();
echo "Inner returned: {$result}\n";
yield 3;
}
foreach (outerGen() as $value) {
echo $value . ' '; // 0 1 2 3
}
// Inner returned: inner done
// Yield from arrays
function mixedGen(): Generator {
yield from [10, 20, 30];
yield from new ArrayObject([40, 50]);
}Casos de Uso Prácticos
Los generadores destacan en procesar flujos de datos grandes o infinitos: leer archivos línea por línea, iteración de cursor de base de datos, fetching de APIs paginadas y secuencias matemáticas. El patrón take() limita un generador infinito. Los generadores componen bien: pipee datos a través de múltiples generadores para filtrado, mapeo y reducción. La memoria se mantiene constante independientemente del tamaño de los datos.
<?php
// 1. Read large files line by line (memory efficient)
function readLines(string $file): Generator {
$handle = fopen($file, 'r');
while (!feof($handle)) {
$line = fgets($handle);
if ($line !== false) yield rtrim($line);
}
fclose($handle);
}
foreach (readLines('large.log') as $line) {
if (str_contains($line, 'ERROR')) {
echo "{$line}\n";
}
}
// 2. Infinite sequence with take()
function naturals(): Generator {
$n = 1;
while (true) yield $n++;
}
function take(Generator $gen, int $n): Generator {
for ($i = 0; $i < $n; $i++) {
yield $gen->current();
$gen->next();
}
}
foreach (take(naturals(), 5) as $num) {
echo $num . ' '; // 1 2 3 4 5
}Seguridad
Prevención de Inyección SQL
La inyección SQL ocurre cuando la entrada de usuario se concatena en SQL. Siempre use sentencias preparadas con consultas parametrizadas. PDO y MySQLi ambos las soportan. Nunca confíe en entrada de usuario. Valide y sanitize todos los datos externos.
// BAD: vulnerable
$sql = "SELECT * FROM users WHERE name = '" . $_POST['name'] . "'";
// GOOD: prepared statements
$stmt = $pdo->prepare("SELECT * FROM users WHERE name = :name");
$stmt->execute([':name' => $_POST['name']]);
$users = $stmt->fetchAll();Prevención XSS
XSS (Cross-Site Scripting) inyecta scripts maliciosos. htmlspecialchars convierte caracteres especiales a entidades HTML. ENT_QUOTES escapa tanto comillas simples como dobles. Siempre escape al mostrar datos de usuario en HTML. Use cabeceras Content-Security-Policy para defensa en profundidad.
// BAD: output without escaping
echo $_GET['name'];
// GOOD: escape output
echo htmlspecialchars($_GET['name'], ENT_QUOTES, 'UTF-8');
// For HTML attributes
echo 'value="' . htmlspecialchars($value, ENT_QUOTES) . '"';Hashing de Contraseñas
password_hash usa bcrypt (o argon2) con generación automática de salt. Nunca use md5 o sha1 para contraseñas. password_verify verifica una contraseña contra un hash. password_needs_rehash permite actualizar algoritmos de hash. El salt está embebido en la cadena de hash.
// Hash a password
$hash = password_hash('mypassword', PASSWORD_DEFAULT);
// Verify
if (password_verify('mypassword', $hash)) {
echo 'Valid password';
}
// Check if needs rehash
if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
$newHash = password_hash('mypassword', PASSWORD_DEFAULT);
}Protección CSRF
CSRF (Cross-Site Request Forgery) engaña a usuarios para acciones no deseadas. Genere un token aleatorio por sesión. Inclúyalo en formularios como campo oculto. Verifique en POST usando hash_equals (comparación timing-safe). Las cookies SameSite proporcionan protección adicional.
session_start();
if (empty($_SESSION['token'])) {
$_SESSION['token'] = bin2hex(random_bytes(32));
}
// In form
echo '<input type="hidden" name="token" value="' . $_SESSION['token'] . '">';
// Verify
if (!hash_equals($_SESSION['token'], $_POST['token'] ?? '')) {
die('CSRF token mismatch');
}Seguridad de Sesión
cookie_httponly previene acceso JavaScript. cookie_secure asegura solo HTTPS. samesite=Strict previene CSRF. use_strict_mode rechaza IDs de sesión no inicializados. session_regenerate_id previene fijación de sesión. Siempre regenere después de cambios de privilegios.
// Secure session settings
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1); // HTTPS only
ini_set('session.cookie_samesite', 'Strict');
ini_set('session.use_strict_mode', 1);
session_start();
// Regenerate ID after login
session_regenerate_id(true);Fragmentos de PHP relacionados
Copy-paste ready code for common tasks.
Arrays y Funciones de Array en PHP
Crear arrays indexados, asociativos y multidimensionales con map y filter.
Funciones de String en PHP
Manipular strings con substr, replace, explode y sprintf en PHP.
Leer y Escribir Archivos en PHP
Leer, escribir, añadir e iterar archivos con funciones del sistema de archivos de PHP.
Consultas de Base de Datos con PDO en PHP
Conectar y ejecutar sentencias preparadas de forma segura con PDO en PHP.
Sesiones y Cookies en PHP
Almacenar datos de usuario entre peticiones con sesiones y cookies en PHP.
Clases y Herencia en PHP
Definir clases con constructores, visibilidad y herencia en PHP.
Was this helpful?