Guía de PHP: De 0 a 100

Guía de PHP: De 0 a 100

Desde la primera línea de código hasta producción: instalación, ecosistema, frameworks, proyectos reales y recursos en español.

FECHA:
ACTUALIZADO:
AUTOR:Jorge Beneyto Castelló
LECTURA:95 MINUTOS DE LECTURA

Guía de PHP: De 0 a 100

¿Qué es PHP?

PHP (PHP: Hypertext Preprocessor) es un lenguaje de programación interpretado, de tipado débil/gradual y multiparadigma, diseñado originalmente para el desarrollo web del lado del servidor. Fue creado por Rasmus Lerdorf en 1994 como un conjunto de scripts Perl para mantener su página personal, evolucionando rápidamente a PHP/FI (Personal Home Page / Forms Interpreter) y luego a una implementación completa con el motor Zend Engine (PHP 4, 2000).

¿Dónde se usa?

  • Web (server-side scripting): ~77% de todos los sitios web con servidor conocido usan PHP — WordPress (~43% de toda la web), Laravel, Symfony, Drupal, Joomla
  • APIs REST/GraphQL: Laravel con API Resources, Symfony con API Platform, Laravel Sanctum para auth
  • E-commerce: Magento (Adobe Commerce), WooCommerce (plugin WordPress), PrestaShop, Shopware
  • CMS y portales: WordPress (blog/corporativo), Drupal (gubernamental/educativo), Joomla, TYPO3
  • CLI y devops: Composer (gestor de dependencias), PHPUnit (testing), PHPStan/Psalm (análisis estático), Laravel Artisan
  • Microservicios: Laravel Lumen, Slim, Flight, Mezzio (ex-Zend Expressive)
  • Tiempo real: Ratchet (WebSockets), Swoole (async server), ReactPHP (event loop), amphp

¿Quién lo usa? Facebook (HHVM reemplazó Zend con JIT, aunque ahora usan Hack propio), Wikipedia (PHP + MediaWiki), WordPress.com (.org también), Slack (versión inicial en PHP), Etsy, Mailchimp, Yahoo, Tumblr (versión inicial), Tesla (tienda online en Magento), Pfizer, Adobe (Magento Commerce).

Fuente: W3Techs — Usage Statistics of PHP, PHP.net — History, Wikipedia — PHP

Prerrequisitos

Antes de empezar, necesitas:

  • Conocimientos básicos de terminal: navegar directorios, ejecutar comandos, usar un editor de texto
  • El lenguaje/herramienta instalado en tu sistema (sigue la sección de Instalación si aún no lo tienes)

Si no cumples algún requisito, no te preocupes: cada sección te guiará paso a paso.

¿Cómo empezar? Instalación en todos los entornos

Windows

  1. Descarga el instalador desde windows.php.net/download (versión Thread Safe para Apache, Non-Thread Safe para IIS/CGI)
  2. Alternativa todo-en-uno: XAMPP (Apache + PHP + MariaDB + phpMyAdmin) o Laragon (moderno, portable, con Nginx/Apache, MySQL, Redis, Composer incluido)
  3. Laragon recomendado: pesa ~40 MB, menú contextual, entornos aislados, Node.js + npm integrado
  4. Verifica: abre PowerShell y ejecuta php -v
  5. Alternativa vía winget: winget install PHP.PHP.8.3

macOS

  • Homebrew (recomendado): brew install php — instala PHP 8.x con extensión mbstring, xml, curl
  • Laravel Herd: entorno PHP todo-en-uno con Nginx, PHP 8.x, Composer, DB (brew install herd)
  • MAMP: clásico Apache + PHP + MySQL, interfaz gráfica
  • Verifica: php -v
  • Servidor integrado: php -S localhost:8000

Linux (Debian/Ubuntu)

sudo apt update
sudo apt install php php-cli php-mbstring php-xml php-curl php-mysql php-sqlite3 php-bcmath php-gd php-zip unzip

Para Fedora/RHEL: sudo dnf install php php-cli php-mbstring php-xml php-curl php-mysqlnd php-sqlite3 php-bcmath php-gd php-zip

phpbrew (Linux/macOS/WSL) — versión múltiple

curl -L https://raw.githubusercontent.com/phpbrew/phpbrew/master/phpbrew | bash
# Añade al ~/.bashrc:
echo 'source ~/.phpbrew/bashrc' >> ~/.bashrc
exec $SHELL

phpbrew known                 # Lista versiones disponibles
phpbrew install 8.3 +default  # Compila PHP 8.3 con extensiones por defecto
phpbrew switch 8.3            # Activa PHP 8.3 globalmente

Docker

FROM php:8.3-cli AS base
WORKDIR /app
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
RUN docker-php-ext-install pdo_mysql mbstring bcmath

FROM base AS dev
RUN pecl install xdebug && docker-php-ext-enable xdebug

FROM base AS prod
COPY . /app
RUN composer install --no-dev --optimize-autoloader
CMD ["php", "public/index.php"]

Usa php:8.3-fpm-alpine para producción con Nginx (~40 MB). Evita php:latest para builds reproducibles.

Alias útiles para el día a día

Añade a tu ~/.bashrc o ~/.zshrc:

alias phpv='php -v'
alias php-serve='php -S localhost:8000'
alias php-repl='php -a'
alias cda='composer dump-autoload -o'
alias cdu='composer update'
alias cdi='composer install'
alias ci='composer init'
alias phpstan='vendor/bin/phpstan analyse src --level=max'
alias pint='vendor/bin/pint'
Fuente: phpbrew GitHub, Docker Official Images — PHP, Laragon

Escala de aprendizaje: de 0 a 100

Cada nivel incluye lo que debes saber y un proyecto para fijar los conceptos.

Nivel 0–15: Fundamentos absolutos

Qué aprender:

  • Sintaxis básica: etiquetas <?php ?>, echo, print, variables con $, tipos (int, float, string, bool, null)
  • Tipos de datos escalares y compuestos (ver sección de tipos)
  • Condicionales: if/elseif/else, operadores de comparación (==, ===, !=, !==)
  • Bucles: for, foreach, while
  • Arrays básicos: indexados $arr = [1, 2, 3], asociativos $user = ['nombre' => 'Ana'], count(), isset(), unset()
  • Funciones: function, return, parámetros con tipo
  • Conceptos clave: PHP es de tipado débil — "1" + 1 da 2 (coerción automática). Usa === para comparación estricta (compara valor Y tipo). Los arrays son tablas hash: isset() es O(1), in_array() es O(n). var_dump() y print_r() para debugging.

Proyecto: Calculadora CLI — el programa pide dos números y una operación (+, -, *, /). Maneja división entre cero y entrada no numérica. Muestra resultado. Reflexión: ¿por qué 0 == "0" da true pero 0 === "0" da false?

Nivel 15–30: Arrays, strings, paradigmas y sistema de archivos

Qué aprender:

  • Paradigmas de PHP: imperativo/procedural, POO básico, funcional parcial con closures y arrow functions
  • Sistema de archivos: file_get_contents, file_put_contents, fopen/fread/fwrite, fgets, is_file, file_exists, mkdir, scandir, glob
  • JSON: json_encode, json_decode
  • Array functions: array_map, array_filter, array_reduce, array_merge, array_search, sort/asort/ksort
  • Strings: strlen, strpos, substr, str_replace, explode, implode, trim, strtoupper, strtolower
  • Funciones variables, funciones anónimas (closures), fn() arrow functions (PHP 7.4+)
  • ... spread operator en arrays (PHP 7.4+)
  • compact(), extract(), list() / [$a, $b] = $array
  • Include/require: require_once, include_once, diferencia y cuándo usar cada uno
  • Superglobals: $_GET, $_POST, $_SERVER, $_SESSION, $_COOKIE
  • Conceptos clave: isset() vs empty() — empty() devuelve true para "", 0, null, false, []. ?? (null coalescing) vs @ (suppressor) — nunca uses @. Los closures capturan variables con use (&$var). compact() crea array a partir de variables del scope actual.

Proyecto: Gestor de tareas CLI — añadir (php tareas.php add "Comprar leche"), listar (list), completar (done 3), eliminar (rm 3). Persistencia en JSON con file_get_contents/file_put_contents. Usa $_SERVER['argv'] para argumentos.

Nivel 30–45: POO, namespaces, algoritmos y SPL

Qué aprender:

  • Clases: class, __construct(), $this, propiedades y métodos
  • Visibilidad: public, protected, private
  • Constructor promotion (PHP 8.0): function __construct(public string $nombre) {}
  • Herencia: extends, parent::, final
  • Interfaces: interface, implements
  • Traits (PHP 5.4+): trait, use
  • Clases abstractas: abstract class, abstract function
  • Namespaces: namespace, use, use ... as
  • Autoloading PSR-4 con Composer: composer dump-autoload
  • Excepciones: try/catch/finally, throw, crear excepciones personalizadas
  • Sistema de archivos avanzado: CSV, ZIP, GZ, Fileinfo para MIME, SplFileInfo, SplFileObject
  • Algoritmos con SPL: SplStack, SplQueue, SplHeap, SplFixedArray, SplObjectStorage
  • Array functions avanzadas: array_intersect, array_diff, array_column, array_walk, array_chunk, array_count_values
  • Complejidad Big O de arrays: lookup O(1), búsqueda O(n), sort O(n log n)
  • Ordenación personalizada con usort, uasort, uksort
  • Conceptos clave: PHP no tiene sobrecarga de métodos (como Java/C++). Los traits resuelven el problema de herencia múltiple sin los problemas del diamante. PSR-4 mapea namespace → directorio: App\Models\User → src/Models/User.php. parent:: llama al método de la clase padre. final impide herencia/override. Las estructuras SPL son más eficientes que arrays nativos para casos concretos.

Proyecto: Sistema de inventario — clases Producto, Inventario con namespaces. Métodos: agregar(), retirar(), buscar(), valor_total(). Persistencia con JSON. Excepciones personalizadas (StockInsuficiente, ProductoNoExiste). Autoloading PSR-4 con Composer.

Nivel 45–60: Testing, análisis estático y empaquetado

Qué aprender:

  • PHPUnit: assertSame(), assertInstanceOf(), data providers, setUp, pruebas de excepciones
  • Pest PHP: alternativa moderna a PHPUnit con sintaxis más expresiva
  • PHPStan: niveles 0–10, PHPDoc con @param, @return, @var
  • Psalm: alternativa a PHPStan con detección de código muerto
  • Laravel Pint / PHP CS Fixer: formateo automático de código (PSR-12)
  • Xdebug: depuración paso a paso, profiling
  • Composer scripts: composer run test, composer run phpstan
  • Conceptos clave: PHPStan nivel max detecta problemas que no imaginabas. Los type hints PHPDoc son más expresivos que los de PHP nativo (@template, @param class-string, @return Collection<User>). declare(strict_types=1) obligatorio en todos los archivos nuevos.

Proyecto: Calculadora empaquetada — paquete instalable con Composer. Tests con PHPUnit (parametrized, excepciones). PHPStan nivel max. Pint para estilo. GitHub Actions ejecutando composer check.

Nivel 60–75: Web APIs, frameworks y multimedia

Qué aprender:

  • Laravel: rutas, controladores, Blade, Eloquent, migraciones, validación, auth
  • Symfony: bundles, Doctrine, Twig, Messenger, EventDispatcher
  • API Platform: REST/GraphQL automático desde entidades Doctrine
  • Laravel API Resources: transformación de respuestas JSON
  • JWT / Sanctum: autenticación para APIs
  • Logging: Monolog (estándar PSR-3), canales, niveles
  • Procesamiento de imágenes con GD y Imagick: redimensionar, recortar, filtros, texto, conversión de formatos
  • Video con FFmpeg desde PHP
  • Metadatos EXIF de fotos con exif_read_data
  • Generación de PDF con TCPDF / FPDF
  • Conceptos clave: Service Container (Laravel) / DI Container (Symfony) — no instancies clases manualmente. El patrón Repository desacopla lógica de BD del negocio. Eloquent es Active Record, Doctrine es Data Mapper — elegir según proyecto. GD es nativo pero limitado; Imagick es más potente pero requiere instalación.

Proyecto: API de recetas — Laravel + Eloquent + PostgreSQL o SQLite. CRUD de recetas e ingredientes. Búsqueda por ingrediente. Autenticación con Sanctum. Tests con PHPUnit. Migraciones con schema builder. Validación con Form Request.

Nivel 75–90: Producción, colas y tiempo real

Qué aprender:

  • Docker multi-etapa para PHP: php:8.3-fpm-alpine + Nginx, composer install --no-dev
  • Docker Compose: app + nginx + mysql + redis + mailpit
  • Colas: Laravel Horizon (Redis), Symfony Messenger, RabbitMQ
  • Tareas programadas: Laravel Scheduler, cron
  • WebSockets: Laravel Echo + Reverb, Soketi, Pusher
  • Caché: Redis, Laravel Cache, Symfony Cache
  • Monitoreo: Laravel Telescope, Symfony Profiler, clockwork
  • Conceptos clave: PHP-FPM tiene un pool de workers — configura pm.max_children según RAM disponible. OpCache es crítico en producción: opcache.enable=1, opcache.revalidate_freq=0. Las colas escalan horizontalmente añadiendo workers.

Proyecto: Dashboard de monitorización — Laravel que recolecta métricas del sistema (CPU, RAM, disco). Redis como cola (Horizon). WebSockets con Laravel Reverb para actualizaciones en vivo. Docker Compose (app + nginx + redis + mysql). Tests de integración.

Nivel 90–100: Arquitectura, alto rendimiento y sistemas distribuidos

Qué aprender:

  • CQRS / Event Sourcing con Laravel + Event Sourcing (Spatie) o Ecotone
  • Event Driven: RabbitMQ / Kafka, publicación de eventos
  • Arquitectura hexagonal / clean architecture en PHP: separación de Domain, Application, Infrastructure
  • API Platform: GraphQL, subscripciones, paginación, filtros avanzados
  • Swoole / RoadRunner: reemplazar PHP-FPM por servidor persistente (10-50x más requests/segundo)
  • Performance: OpCache JIT (PHP 8.0+), preloading, FrankenPHP
  • Observabilidad: OpenTelemetry (tracing + metrics + logs), Prometheus + Grafana
  • Conceptos clave: PHP-FPM muere después de cada request — Swoole/RoadRunner mantiene el estado en memoria (conexiones BD, caché). El JIT de PHP 8.0+ puede acelerar código CPU-bound hasta 3x. FrankenPHP (escrito en Go) sirve PHP directamente como servidor HTTP/HTTPS.

Proyecto: Sistema de notificaciones distribuidas — microservicios con Laravel + RoadRunner. Event bus con RabbitMQ (usuarios, notificaciones). API Platform para GraphQL BFF. FrankenPHP como servidor. OpenTelemetry para tracing distribuido. Docker Compose + Prometheus + Grafana.

Fuente: Laravel docs, Symfony docs, API Platform docs, Swoole docs, RoadRunner docs, FrankenPHP

Primeros pasos y configuración del entorno

Si prefieres ver directamente el “Hola Mundo” en PHP, consulta El Atlas del Hola Mundo.

El REPL

PHP incluye un intérprete interactivo. Ejecuta php -a en terminal y escribe directamente:

php > echo "Hola mundo";
php > $suma = 2 + 2;
php > var_dump($suma);
php > $array = [1, 2, 3];
php > array_map(fn($n) => $n ** 2, $array);

Para salir: exit o Ctrl+C.

Servidor integrado para desarrollo

PHP 5.4+ trae un servidor web integrado. Útil para desarrollo y pruebas sin Apache/Nginx:

php -S localhost:8000                    # Servidor simple
php -S 0.0.0.0:8080 -t public/          # Con document root
php -S localhost:8000 router.php        # Con router personalizado

Scripts y estructura de proyecto

<?php
// hola.php
declare(strict_types=1);

function main(): void
{
    echo "Hola mundo" . PHP_EOL;
}

main();

Ejecuta: php hola.php

Composer y autoloading (gestor de dependencias)

Composer es el gestor de dependencias de facto en PHP, similar a npm (Node), pip (Python) o cargo (Rust).

# Instalación global
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php composer-setup.php
php -r "unlink('composer-setup.php');"
mv composer.phar /usr/local/bin/composer

# Iniciar proyecto
composer init --name="usuario/mi-proyecto" --type=project --no-interaction

# Añadir dependencias
composer require laravel/framework
composer require --dev phpunit/phpunit phpstan/phpstan

# PSR-4 autoloading configurado
composer dump-autoload -o  # -o = optimize (genera classmap)

Archivo composer.json básico:

{
    "name": "usuario/mi-proyecto",
    "require": {
        "php": ">=8.2"
    },
    "require-dev": {
        "phpunit/phpunit": "^11.0",
        "phpstan/phpstan": "^1.10"
    },
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "Tests\\": "tests/"
        }
    },
    "scripts": {
        "test": "phpunit",
        "phpstan": "phpstan analyse src --level=max",
        "lint": "vendor/bin/pint",
        "check": ["@lint", "@phpstan", "@test"]
    }
}

Editores recomendados

Los IDEs y editores más usados son VSCode, JetBrains y Neovim. Extensiones y configuraciones recomendadas.

Configuración inicial del proyecto

composer init --name="app/mi-proyecto" --type=project --no-interaction
composer require --dev phpunit/phpunit phpstan/phpstan laravel/pint

Archivo phpstan.neon:

parameters:
    level: max
    paths:
        - src
    checkGenericClassInNonGenericObjectType: true
    checkMissingIterableValueType: true

Archivo phpunit.xml:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true"
         cacheDirectory=".phpunit.cache">
    <testsuites>
        <testsuite name="Unit">
            <directory>tests/Unit</directory>
        </testsuite>
    </testsuites>
    <source>
        <include>
            <directory>src</directory>
        </include>
    </source>
</phpunit>

Xdebug (depurador paso a paso)

# Linux (PECL)
pecl install xdebug

# Verificar instalación
php -v | grep Xdebug

# Configuración php.ini
zend_extension=xdebug.so
xdebug.mode=develop,debug,profile
xdebug.start_with_request=yes
xdebug.idekey=VSCODE
xdebug.client_host=127.0.0.1
xdebug.client_port=9003
Fuente: Composer docs, Xdebug docs, PHP Intelephense

Paradigmas en PHP

PHP es un lenguaje multiparadigma. Puedes (y acabarás) usando varios paradigmas en un mismo proyecto.

Imperativo / Procedural

Es el paradigma original de PHP. La ejecución es secuencial, se usan funciones y variables globales. Fue el estilo dominante hasta PHP 4 (2000) y sigue presente en código legacy y scripts simples.

<?php
$nombre = "Mundo";
echo "Hola, $nombre";

POO (Programación Orientada a Objetos)

PHP 5 (2004) trajo un modelo de objetos completo. PHP 8.x lo ha refinado con constructor promotion, readonly, enums, etc.

<?php
declare(strict_types=1);

interface Logger
{
    public function log(string $msg): void;
}

trait Timestampable
{
    public function timestamp(): string
    {
        return date('Y-m-d H:i:s');
    }
}

abstract class Service
{
    abstract public function handle(): void;
}

class UserService extends Service implements Logger
{
    use Timestampable;

    public function __construct(
        private string $name,
    ) {}

    public function log(string $msg): void
    {
        echo "[{$this->timestamp()}] $msg";
    }

    public function handle(): void
    {
        $this->log("Procesando {$this->name}");
    }
}

Funcional (parcial)

PHP tiene soporte funcional limitado pero útil: closures, arrow functions, y funciones de orden superior.

<?php
$numbers = [1, 2, 3, 4, 5];

$duplicar = function (int $n): int {
    return $n * 2;
};

$cuadrado = fn(int $n): int => $n ** 2;

$pares = array_filter($numbers, fn($n) => $n % 2 === 0);
$doblados = array_map(fn($n) => $n * 2, $numbers);
$suma = array_reduce($numbers, fn($c, $n) => $c + $n, 0);

Declarativo (SQL embebido)

PHP nació para generar HTML con acceso a BD. Históricamente, el SQL se mezclaba directamente con el código PHP.

<?php
$link = mysqli_connect('localhost', 'user', 'pass', 'db');
$result = mysqli_query($link, 'SELECT * FROM usuarios WHERE activo = 1');
while ($row = mysqli_fetch_assoc($result)) {
    echo $row['nombre'];
}
Fuente: PHP Manual — OOP, PHP Manual — Anonymous Functions, Wikipedia — PHP

Tipos de datos y variables

Variables

¿Qué es?

En PHP, toda variable empieza por $. No necesitan declaración previa ni tipo explícito — el tipado es dinámico aunque desde PHP 5 se pueden declarar tipos en funciones y desde PHP 7.4 en propiedades. La primera asignación define el tipo y el contenido.

🔗 PHP Manual — Variables

Sintaxis básica

<?php
$nombre = "Ana";        // string
$edad = 28;             // int
$precio = 29.99;        // float
$activo = true;         // bool
$datos = null;          // null
ConvenciónEjemploÁmbito
$variable$nombreLocal
$GLOBALS['var']$GLOBALS['db']Global
$_GET, $_POST$_GET['id']Superglobal
$this->prop$this->nombreDe instancia

🧪 Cómo probarlo

<?php
// variables.php
$nombre = "Ana";
$edad = 28;
var_dump($nombre); // string(3) "Ana"
var_dump($edad);   // int(28)
echo '$nombre vale ' . $nombre;

// Test con PHPUnit
// vendor/bin/phpunit --filter testVariables
// public function testVariables(): void {
//     $nombre = "Ana";
//     $this->assertSame("Ana", $nombre);
// }

💡 Memoria y rendimiento

  • Las variables son contenedores zval que almacenan tipo, valor y refcount
  • Copy-on-write: asignar $b = $a no copia hasta que se modifica
  • unset() decrementa refcount, el GC libera cuando llega a 0
  • Las superglobales ($_GET, $_POST, $_SERVER) existen siempre en request — no las reasignes sin necesidad

✅ Buenas prácticas

  • Usa nombres descriptivos en camelCase o snake_case (elige uno y sé consistente)
  • ✅ Declara tipos en propiedades y funciones desde PHP 7.4+
  • ✅ Inicializa variables explícitamente
  • ❌ Nunca uses extract() en datos del usuario — permite inyección de variables
  • ❌ No confíes en register_globals (eliminado en PHP 5.4)
  • ⚠️ $_GET, $_POST contienen strings — conviértelos explícitamente con (int) o intval()

🏗️ Metodología

Las variables en PHP se usan en todos los paradigmas. En scripts pequeños, las globales son aceptables. En proyectos grandes (>10k líneas), encapsula todo en clases o funciones con inyección de dependencias.

🔗 Para saber más

Tipos escalares

¿Qué es?

PHP tiene cuatro tipos escalares: int, float, string y bool. Son los bloques básicos sobre los que se construyen todos los demás tipos. Internamente, el Zend Engine los representa como estructuras C dentro del contenedor zval.

🔗 PHP Manual — Scalar Types

Sintaxis básica

TipoDesdeEjemploDescripción
intPHP 442Entero 64 bits
floatPHP 43.14Punto flotante IEEE 754
stringPHP 4"hola"Secuencia de bytes
boolPHP 4trueBooleano
<?php
$entero = 42;
$flotante = 3.14;
$texto = "Hola";
$booleano = true;

🧪 Cómo probarlo

php -r 'var_dump(is_int(42), is_float(3.14), is_string("a"), is_bool(true));'

💡 Memoria y rendimiento

TipoTamaño (64-bit)Notas
int8 bytesPrecisión exacta hasta 9.007.199.254.740.991
float8 bytesIEEE 754 double — no exacto para decimales
bool1 byteAlmacenado como zend_long internamente
stringVariable1 byte por carácter + overhead zval

✅ Buenas prácticas

  • Usa === para comparar escalares (compara tipo y valor)
  • Convierte explícitamente: (int) $input, strval(), floatval()
  • ⚠️ 0 == "foo" devuelve true en modo débil — usa ===
  • Prefiere declare(strict_types=1) en archivos nuevos

🏗️ Metodología

Los escalares son la base de cualquier programa PHP. Úsalos directamente para datos simples. Para datos complejos con validación, envuélvelos en Value Objects.

🔗 Para saber más

Strings

¿Qué es?

Los strings en PHP son secuencias de bytes. No hay codificación Unicode nativa — para UTF-8 se necesita la extensión mbstring (multibyte string). PHP soporta 4 sintaxis distintas de strings: comillas simples, dobles, heredoc y nowdoc.

🔗 PHP Manual — Strings

Sintaxis básica

<?php
$nombre = "Ana";
$s1 = 'Hola $nombre';        // literal: "Hola $nombre"
$s2 = "Hola $nombre";        // interpolado: "Hola Ana"
$s3 = <<<TEXT                // heredoc
Línea 1
Línea 2
TEXT;
$s4 = <<<'TEXT'              // nowdoc (sin interpolación)
Línea $nombre
TEXT;

echo mb_strlen('ññ');        // 2 (no 4 como strlen)
echo mb_substr('café', 2);   // 'fé'

🧪 Cómo probarlo

<?php
// strings.php
$nombre = "Ana";
$saludo = "Hola, $nombre";
assert($saludo === "Hola, Ana");

$longitud = mb_strlen('ññ');
assert($longitud === 2);

💡 Memoria y rendimiento

  • Las strings son mutables pero la reasignación crea una nueva
  • mbstring es ~2x más lento que strlen/substr pero necesario para UTF-8
  • Para concatenación masiva, usa implode() o sprintf() en vez de .= en bucles
  • Las strings entre comillas dobles se parsean en busca de variables — es marginalmente más lento que comillas simples

✅ Buenas prácticas

  • Usa mb_* funciones para cualquier texto con acentos/ñ/emojis
  • Prefiere sprintf() o implode() sobre concatenación con .
  • ⚠️ strlen('café') da 5 (no 4) porque ‘é’ ocupa 2 bytes en UTF-8
  • ❌ No uses $$ para variables variables con strings del usuario

🏗️ Metodología

Las strings en PHP son el tipo más ubicuo — se usan para HTML, SQL, JSON, rutas. Para textos cortos usa comillas simples. Para textos con formato usa heredoc. Para internacionalización, encapsula strings en funciones __() o similar.

🔗 Para saber más

Arrays

¿Qué es?

Los arrays de PHP son mapas ordenados — internamente son tablas hash que mantienen el orden de inserción. Pueden funcionar como listas, diccionarios, pilas, colas, conjuntos, etc. Es la estructura de datos más versátil y la más usada en PHP.

🔗 PHP Manual — Arrays

Sintaxis básica

<?php
$nums = [1, 2, 3, 4, 5];
$user = ['nombre' => 'Ana', 'edad' => 28];

$cuadrados = array_map(fn($n) => $n ** 2, $nums);
$pares = array_filter($nums, fn($n) => $n % 2 === 0);
$suma = array_reduce($nums, fn($c, $n) => $c + $n, 0);

$merged = array_merge([1, 2], [3, 4]);
$keys = array_keys($user);
$vals = array_values($user);
$pos = array_search(3, $nums);
$existe = in_array(3, $nums);

sort($nums);  rsort($nums);  asort($nums);  ksort($nums);
usort($nums, fn($a, $b) => $a <=> $b);

🧪 Cómo probarlo

<?php
$nums = [1, 2, 3, 4, 5];
$pares = array_filter($nums, fn($n) => $n % 2 === 0);
assert($pares === [2, 4]);

$suma = array_reduce($nums, fn($c, $n) => $c + $n, 0);
assert($suma === 15);

💡 Memoria y rendimiento

OperaciónComplejidadNotas
Acceso $arr[$key]O(1) promedioTabla hash
Inserción $arr[] = $valO(1) amortizado
in_array() / array_search()O(n)Búsqueda lineal
isset($arr[$key])O(1)
sort()O(n log n)Quicksort
array_map() / array_filter()O(n)

Cada array tiene overhead ~56 bytes + 32 bytes por elemento (zval + hash bucket).

✅ Buenas prácticas

  • ✅ Usa array_map/array_filter/array_reduce en lugar de foreach para transformaciones
  • ✅ isset() es O(1), prefiérelo sobre array_key_exists() con valores null
  • ⚠️ foreach con & (referencia) requiere unset() después para evitar efectos laterales
  • ❌ No uses extract() — dificulta el seguimiento de variables
  • ❌ No mezcles índices numéricos y asociativos en el mismo array

🏗️ Metodología

Los arrays en PHP son todoterreno. Para datos simples: arrays numéricos. Para registros: arrays asociativos. Para colecciones grandes (>100k elementos): SplFixedArray. Para pilas/colas: SplStack/SplQueue. Para conjuntos: usa claves de array como $set[$valor] = true.

🔗 Para saber más

Gestión de memoria en PHP

¿Qué es?

PHP usa el Zend Engine con un administrador de memoria propio que gestiona zval (contenedores de valor), copy-on-write y garbage collection cíclico. Es importante entenderlo para escribir código eficiente y evitar fugas de memoria.

🔗 PHP Manual — GC

Sintaxis básica

<?php
$a = range(1, 1000000);
$b = $a;       // refcount = 2, sin copia (copy-on-write)
$b[0] = 999;   // aquí se copia (separación)
unset($a);     // decrementa refcount

🧪 Cómo probarlo

php -r '
$a = range(1, 1000);
$b = $a;
debug_zval_ref($a);
'

💡 Memoria y rendimiento

  • zval: 16 bytes + 8 bytes por valor (tipos simples) + hash bucket (~24 bytes en arrays)
  • Copy-on-write: asignar no copia hasta que modificas — ahorra memoria masivamente
  • GC cíclico: no existía hasta PHP 5.3; desde PHP 7.3+ está siempre activo
  • ⚠️ Referencias circulares: objetos que se apuntan mutuamente no se liberan hasta el GC cíclico
  • memory_get_usage() y memory_get_peak_usage() para monitorizar

✅ Buenas prácticas

  • ✅ unset() grandes estructuras cuando ya no las necesites
  • ✅ Usa SplFixedArray para arrays grandes de tamaño fijo
  • ⚠️ Las referencias (&) rompen el copy-on-write
  • ⚠️ Los generadores (yield) mantienen memoria constante incluso con datos grandes

🏗️ Metodología

Para scripts pequeños (&lt;1k líneas) no te preocupes por la memoria. Para aplicaciones grandes, usa profiling con Xdebug y monitoriza con memory_get_usage(). Los generadores son la herramienta clave para procesar archivos grandes (logs, CSV) sin agotar memoria.

🔗 Para saber más


Control de flujo y modularidad

Condicionales

¿Qué es?

PHP ofrece las estructuras típicas de control: if/elseif/else, switch, y desde PHP 8.0 la expresión match. Las condicionales controlan qué bloques de código se ejecutan según una condición booleana.

🔗 PHP Manual — Control Structures

Sintaxis básica

<?php
$edad = 18;

if ($edad < 12) {
    echo "Niño";
} elseif ($edad < 18) {
    echo "Adolescente";
} else {
    echo "Adulto";
}

switch ($edad) {
    case 18:
        echo "Recién adulto";
        break;
    default:
        echo "Otra edad";
}

$categoria = match (true) {
    $edad < 12 => 'Niño',
    $edad < 18 => 'Adolescente',
    $edad < 65 => 'Adulto',
    default    => 'Senior',
};
EstructuraCuándo usarla
if/elseif/elseCondiciones complejas con operadores lógicos
switchUna variable contra múltiples valores fijos
match (PHP 8.0)Switch mejorado con tipado estricto y sin break

🧪 Cómo probarlo

php -r '
$edad = 18;
$cat = match (true) { $edad < 12 => "Niño", $edad < 65 => "Adulto", default => "Senior" };
var_dump($cat);
'

💡 Memoria y rendimiento

  • match es tan rápido como switch pero más seguro (tipado estricto)
  • if/elseif con muchas ramas (>10) puede beneficiarse de un array mapa en vez de condicionales
  • switch con break olvidado es la fuente #1 de bugs en condicionales

✅ Buenas prácticas

  • ✅ Prefiere match sobre switch desde PHP 8.0
  • ⚠️ Nunca olvides break en switch
  • ✅ Usa early returns para evitar anidamiento profundo
  • ❌ No uses else después de return — es código muerto

🏗️ Metodología

Para 2-3 ramas usa if/else. Para múltiples valores fijos usa match. Para lógica compleja con side effects usa if con guards. En código funcional, prefiere match como expresión que retorna valor.

🔗 Para saber más

Bucles

¿Qué es?

PHP soporta 4 tipos de bucles: for, foreach, while y do/while. foreach es el más usado por su sintaxis limpia con arrays. Los bucles pueden anidarse y usar break N para salir de múltiples niveles.

🔗 PHP Manual — Loops

Sintaxis básica

<?php
$nums = [1, 2, 3, 4, 5];

for ($i = 0; $i < count($nums); $i++) {
    echo $nums[$i];
}

foreach ($nums as $valor) {
    echo $valor;
}

foreach ($nums as &$valor) {
    $valor *= 2;
}
unset($valor);

$user = ['nombre' => 'Ana', 'edad' => 28];
foreach ($user as $clave => $valor) {
    echo "$clave: $valor";
}

$i = 0;
while ($i < 5) {
    echo $i++;
}

$i = 0;
do {
    echo $i++;
} while ($i < 5);

for ($i = 0; $i < 10; $i++) {
    for ($j = 0; $j < 10; $j++) {
        if ($i === 2 && $j === 3) break 2;
    }
}

🧪 Cómo probarlo

php -r '
$nums = [1, 2, 3];
$sum = 0;
foreach ($nums as $n) $sum += $n;
var_dump($sum); // int(6)
'

💡 Memoria y rendimiento

  • foreach sobre arrays es O(n) — tan eficiente como for
  • for ($i = 0; $i < count($arr); $i++) evalúa count() en cada iteración — guarda el tamaño en variable
  • foreach con & (referencia) mantiene la referencia después del bucle — siempre usa unset()
  • Los generadores (yield) evitan construir el array completo en memoria

✅ Buenas prácticas

  • ✅ foreach sobre for para arrays — más legible y seguro
  • ⚠️ Siempre unset() la referencia después de foreach (&$v)
  • ✅ break 2 para salir de bucles anidados
  • ❌ No modifiques el array mientras iteras con foreach

🏗️ Metodología

Usa foreach para recorrer colecciones (95% de los casos). Usa for cuando necesites un índice numérico. Usa while para buques basados en condición (lectura de archivos, streams). Usa do/while cuando necesites ejecutar al menos una vez.

🔗 Para saber más

Excepciones

¿Qué es?

PHP tiene un sistema de excepciones con try/catch/finally. Desde PHP 7.1 puedes capturar múltiples tipos en un mismo catch con pipe (|). Throwable es la interfaz base que implementan Exception y Error.

🔗 PHP Manual — Exceptions

Sintaxis básica

<?php
try {
    if (!file_exists('datos.json')) {
        throw new \RuntimeException('Archivo no encontrado');
    }
    $data = file_get_contents('datos.json');
} catch (\RuntimeException $e) {
    echo "Error: {$e->getMessage()}";
} catch (\Throwable $e) {
    echo "Error inesperado: {$e->getMessage()}";
} finally {
    echo "Limpieza";
}

$nombre = $_GET['name'] ?? throw new \InvalidArgumentException('Name required');

🧪 Cómo probarlo

<?php
// exceptions.php
function dividir(int $a, int $b): float {
    if ($b === 0) throw new \DivisionByZeroError('División por cero');
    return $a / $b;
}

try {
    echo dividir(10, 0);
} catch (\DivisionByZeroError $e) {
    echo "Error: " . $e->getMessage();
}

💡 Memoria y rendimiento

  • Lanzar excepciones es lento (~1000x más que un if) — úsalas para errores excepcionales, no para control de flujo
  • El bloque finally se ejecuta siempre, incluso si hay return en try
  • throw como expresión (PHP 8.0) permite lanzar en lugares donde antes no se podía (operador ternario, null coalescing)

✅ Buenas prácticas

  • ✅ Captura excepciones específicas, no \Exception a secas
  • ✅ Usa finally para liberar recursos (archivos, BD)
  • ⚠️ No uses excepciones para control de flujo normal
  • ✅ Personaliza excepciones extendiendo \RuntimeException o \DomainException

🏗️ Metodología

Las excepciones son para situaciones excepcionales. Para validación de input del usuario, prefiere devolver errores. Para errores de infraestructura (BD caída, archivo no encontrado), la excepción es correcta. En APIs, captura en un middleware global.

🔗 Para saber más

Namespaces y autoloading

¿Qué es?

Los namespaces organizan el código PHP evitando colisiones de nombres. El autoloading carga clases automáticamente sin require manual. PSR-4 es el estándar de la comunidad y Composer lo implementa por defecto.

🔗 PHP Manual — Namespaces

Sintaxis básica

<?php
// src/Models/Usuario.php
namespace App\Models;

use App\Core\Model;
use function array_map;
use const PHP_EOL;

class Usuario extends Model {}
<?php
require_once __DIR__ . '/vendor/autoload.php';
include_once 'config.php';
{
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        },
        "classmap": ["src/Legacy"],
        "files": ["src/helpers.php"]
    }
}
<?php
spl_autoload_register(function (string $class): void {
    $file = __DIR__ . '/src/' . str_replace('\\', '/', $class) . '.php';
    if (file_exists($file)) {
        require_once $file;
    }
});

🧪 Cómo probarlo

# Verifica autoloading
php -r '
require "vendor/autoload.php";
var_dump(class_exists("App\\Models\\Usuario"));
'

💡 Memoria y rendimiento

  • Composer genera un classmap optimizado con composer dump-autoload -o
  • El autoloading PSR-4 con optimización convierte todo a classmap (O(1) lookup)
  • include_once es ligeramente más lento que include por la comprobación
  • require es igual de rápido que include pero para errores fatales

✅ Buenas prácticas

  • ✅ Sigue PSR-4 estrictamente: App\Models\Usuario → src/Models/Usuario.php
  • ✅ Ejecuta composer dump-autoload -o en producción
  • ✅ Siempre require __DIR__ . '/vendor/autoload.php' en el entry point
  • ❌ No mezcles require, include y autoloading — usa solo Composer

🏗️ Metodología

Para proyectos pequeños (&lt;5 clases), el autoloading manual con spl_autoload_register basta. Para cualquier proyecto que use dependencias externas, Composer es obligatorio. Usa PSR-4 como convención de nombres — es el estándar de la industria.

🔗 Para saber más


Conceptos clave explicados a fondo

Tipado débil/gradual y sistema de tipos

PHP tradicionalmente es de tipado dinámico: una variable puede cambiar de tipo en cualquier momento. Sin embargo, desde PHP 7.0 (2015) se introdujeron declaraciones de tipo escalar (int, string, float, bool), y desde PHP 7.4 (2019) typed properties en clases.

PHP 8.0 (2020) trajo union types, mixed, y el atributo #[\\ReturnTypeWillChange]. PHP 8.1 (2021) añadió never, readonly properties, y intersection types (en interfaces). PHP 8.2 (2022) introdujo null como tipo independiente y true/false como tipos independientes.

strict_types — el modo estricto:

<?php
declare(strict_types=1);

function suma(int $a, int $b): int {
    return $a + $b;
}

echo suma(1, 2);    // 3
echo suma("1", 2);  // TypeError: Argument #1 ($a) must be of type int, string given

Sin declare(strict_types=1), PHP coerrea automáticamente: suma("1", 2) devuelve 3 sin error (coerción débil). Con strict types, los tipos se verifican estrictamente.

Tabla de coerciones (modo débil vs estricto):

DeclaraciónLlamadaModo débilModo estricto
int $asuma("1")1 (coerción)TypeError
string $asaluda(42)"42"TypeError
bool $aactivo(1)trueTypeError
float $aprecio("10.5")10.5TypeError
int|string $afoo(null)TypeErrorTypeError (null no está en union)

Tipos disponibles en PHP 8.3+:

  • Escalares: int, float, string, bool
  • Compuestos: array, object, callable, iterable
  • Especiales: mixed (cualquier tipo), never (nunca retorna), void (no retorna valor), null (solo null), true, false
  • Uniones: int|string|float, string|null (equivalente a ?string)
  • Intersecciones: Countable&ArrayAccess (PHP 8.1+)
  • DNF (Disjunctive Normal Form): (Countable&ArrayAccess)|string (PHP 8.2+)
Fuente: PHP Manual — Type System, PHP 8.0 — Union Types RFC

Variables, alcance y variables variables

PHP usa el prefijo $ para todas las variables. No hay declaración explícita: una variable existe desde la primera asignación.

Alcance (scope):

<?php
$global = "soy global";  // Variable global

function test(): void {
    $local = "soy local";  // Variable local a la función
    
    // Para acceder a global dentro de una función:
    global $global;
    echo $global;
    
    // O mediante $GLOBALS:
    echo $GLOBALS['global'];
    
    // Variable estática: persiste entre llamadas
    static $contador = 0;
    $contador++;
    echo $contador;  // 1, 2, 3...
}

test();
echo $local;  // Warning: Undefined variable $local (no accesible fuera)

Variables variables ($$):

<?php
$nombre = "framework";
$$nombre = "Laravel";  // Crea $framework = "Laravel"
echo $framework;       // "Laravel"
echo ${$nombre};       // "Laravel"
echo ${$nombre[0]};    // Accede a $f (primera letra de "framework")

// Útil para acceso dinámico
$tipo = "db";
${$tipo . "_host"} = "localhost";  // Crea $db_host = "localhost"

Por qué PHP usa $?

El $ diferencia variables de funciones/constantes. foo() es una función, $foo es una variable, FOO es una constante. Esto elimina ambigüedades en parsing y permite interpolación directa en strings: "Hola $nombre".

Referencias (&) — paso por referencia:

<?php
function añadir(&$array, $valor): void {
    $array[] = $valor;  // Modifica el array original
}

$nums = [1, 2];
añadir($nums, 3);
print_r($nums);  // [1, 2, 3]

A diferencia de Python (todo pass-by-object-reference), PHP permite paso explícito por referencia con &. Sin referencia, las variables se copian (copy-on-write mediante el zend_value interno).

Fuente: PHP Manual — Variables, PHP Manual — References

Arrays: la estructura de datos más versátil de PHP

Los arrays en PHP son mapas ordenados (tablas hash internamente). Pueden funcionar como listas, diccionarios, pilas, colas, conjuntos, etc.

Indexados y asociativos:

<?php
// Indexado (array list)
$numeros = [1, 2, 3, 4, 5];

// Asociativo (hash map)
$usuario = [
    'nombre' => 'Ana',
    'email' => 'ana@email.com',
    'edad' => 28,
];

// Mixto (no recomendado pero posible)
$mixto = [
    'clave' => 'valor',
    0 => 'cero',
    true => 'uno',  // true se convierte a 1
];

// Arrays multidimensionales
$matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
];

Array functions esenciales:

<?php
$nums = [1, 2, 3, 4, 5];

// Map, filter, reduce
$cuadrados = array_map(fn($n) => $n ** 2, $nums);
$pares = array_filter($nums, fn($n) => $n % 2 === 0);
$suma = array_reduce($nums, fn($carry, $n) => $carry + $n, 0);

// Búsqueda
$pos = array_search(3, $nums);        // 2 (índice), false si no existe
$existe = in_array(3, $nums);         // true/false
$claves = array_keys($usuario);       // ['nombre', 'email', 'edad']
$valores = array_values($usuario);    // ['Ana', 'ana@email.com', 28]

// Ordenación (modifica el array original)
sort($nums);        // Orden ascendente (valores)
rsort($nums);       // Orden descendente
asort($nums);       // Ordena manteniendo asociación clave-valor
ksort($nums);       // Ordena por claves
usort($nums, fn($a, $b) => $a <=> $b);  // Orden personalizado con spaceship

// Manipulación
$slice = array_slice($nums, 0, 3);      // [1, 2, 3] (no modifica original)
$removed = array_splice($nums, 1, 2);   // Elimina desde índice 1, 2 elementos
$merged = array_merge([1, 2], [3, 4]);  // [1, 2, 3, 4]
$diff = array_diff([1, 2, 3], [2, 4]);  // [1, 3] (valores en primero no en segundo)
$intersect = array_intersect([1, 2, 3], [2, 3, 4]); // [2, 3]

// Comprobar tipo
echo is_array($nums);        // true
echo is_list($nums);         // true si índices son 0, 1, 2... secuenciales (PHP 8.1+)
echo array_is_list($nums);   // mismo que is_list

// Spread operator en arrays (PHP 7.4+)
$part1 = [1, 2];
$part2 = [3, 4];
$completo = [...$part1, ...$part2];  // [1, 2, 3, 4]

ArrayAccess y Traversable (objetos como arrays):

<?php
class Coleccion implements ArrayAccess, Countable, Iterator {
    private array $items = [];
    
    public function offsetExists(mixed $offset): bool {
        return isset($this->items[$offset]);
    }
    
    public function offsetGet(mixed $offset): mixed {
        return $this->items[$offset] ?? null;
    }
    
    public function offsetSet(mixed $offset, mixed $value): void {
        if ($offset === null) {
            $this->items[] = $value;
        } else {
            $this->items[$offset] = $value;
        }
    }
    
    public function offsetUnset(mixed $offset): void {
        unset($this->items[$offset]);
    }
    
    public function count(): int {
        return count($this->items);
    }
    
    public function current(): mixed {
        return current($this->items);
    }
    
    public function next(): void {
        next($this->items);
    }
    
    public function key(): mixed {
        return key($this->items);
    }
    
    public function valid(): bool {
        return current($this->items) !== false;
    }
    
    public function rewind(): void {
        reset($this->items);
    }
}

$col = new Coleccion();
$col[] = 'PHP';
$col[] = 'Python';
echo $col[0];  // 'PHP' (via offsetGet)
echo count($col);  // 2 (via count)

foreach ($col as $key => $value) {
    echo "$key: $value";  // 0: PHP, 1: Python
}

SplFixedArray — array de tamaño fijo más eficiente:

<?php
// Más rápido y ocupa menos memoria que array normal
$arr = SplFixedArray::fromArray([1, 2, 3, 4, 5]);
echo $arr[0];      // 1
echo $arr->count(); // 5
$arr->setSize(10);  // Redimensiona (rellena con null)

Complejidad de arrays en PHP (son tablas hash internamente):

OperaciónComplejidadNotas
Acceso $arr[$key]O(1) promedioTabla hash interna
Inserción $arr[] = $valO(1)Amortizado
in_array() / array_search()O(n)Búsqueda lineal
isset($arr[$key])O(1)Verificación hash
sort()O(n log n)Quicksort interno
array_map() / array_filter()O(n)Recorrido completo
array_merge()O(n + m)Copia ambos arrays
Fuente: PHP Manual — Array, PHP Manual — Array Functions

POO: clases, interfaces, traits, namespaces y autoloading

PHP tiene un modelo de objetos muy completo desde PHP 5 (2004). PHP 7+ y 8+ han añadido características modernas.

Clases básicas y constructor promotion (PHP 8.0):

<?php
declare(strict_types=1);

namespace App\Models;

enum Status: string {
    case Active = 'active';
    case Inactive = 'inactive';
    case Archived = 'archived';
}

class Usuario
{
    // Constructor property promotion (PHP 8.0)
    // Crea las propiedades y las asigna automáticamente
    public function __construct(
        private string $nombre,
        private string $email,
        private int $edad,
        private Status $status = Status::Active,
    ) {}
    
    // Getter (tipado de retorno)
    public function nombre(): string
    {
        return $this->nombre;
    }
    
    // Setter con validación
    public function setEdad(int $edad): static
    {
        if ($edad < 0 || $edad > 150) {
            throw new \InvalidArgumentException("Edad inválida: $edad");
        }
        $this->edad = $edad;
        return $this;  // fluent interface
    }
    
    public function esMayorDeEdad(): bool
    {
        return $this->edad >= 18;
    }
}

Interfaces y traits:

<?php
// Interfaz: define un contrato
interface Exportable
{
    public function toArray(): array;
    public function toJson(): string;
}

// Trait: reutilización horizontal de métodos (PHP 5.4+)
trait Timestampable
{
    private \DateTimeImmutable $createdAt;
    private ?\DateTimeImmutable $updatedAt = null;
    
    public function initializeTimestamps(): void
    {
        $this->createdAt = new \DateTimeImmutable();
    }
    
    public function markAsUpdated(): void
    {
        $this->updatedAt = new \DateTimeImmutable();
    }
    
    public function createdAt(): \DateTimeImmutable
    {
        return $this->createdAt;
    }
    
    public function updatedAt(): ?\DateTimeImmutable
    {
        return $this->updatedAt;
    }
}

// Clase abstracta
abstract class Entidad implements Exportable
{
    use Timestampable;
    
    public function __construct(
        private int $id,
    ) {
        $this->initializeTimestamps();
    }
    
    public function id(): int
    {
        return $this->id;
    }
    
    // Método abstracto: las subclases deben implementarlo
    abstract public function validar(): bool;
}

// Implementación concreta
class Producto extends Entidad
{
    public function __construct(
        int $id,
        private string $nombre,
        private float $precio,
    ) {
        parent::__construct($id);
    }
    
    public function validar(): bool
    {
        return $this->precio > 0 && strlen($this->nombre) > 0;
    }
    
    public function toArray(): array
    {
        return [
            'id' => $this->id(),
            'nombre' => $this->nombre,
            'precio' => $this->precio,
        ];
    }
    
    public function toJson(): string
    {
        return json_encode($this->toArray());
    }
}

Herencia múltiple con traits (MRO):

<?php
trait A {
    public function foo(): string { return 'A'; }
}

trait B {
    public function foo(): string { return 'B'; }
}

class MiClase {
    use A, B {
        B::foo insteadof A;  // Usa foo de B en lugar de A
        A::foo as fooA;      // Alias para acceder a foo de A
    }
}

$obj = new MiClase();
echo $obj->foo();   // 'B'
echo $obj->fooA();  // 'A'

Namespaces y autoloading PSR-4:

<?php
// src/Controllers/UsuarioController.php
namespace App\Controllers;

use App\Models\Usuario;
use App\Repositories\UsuarioRepository;

class UsuarioController
{
    public function __construct(
        private UsuarioRepository $repo,
    ) {}
    
    public function index(): array
    {
        return array_map(
            fn(Usuario $u) => $u->toArray(),
            $this->repo->findAll(),
        );
    }
}

Composer autoload (PSR-4) mapea App\\ al directorio src/. Cualquier clase en src/Controllers/ con namespace App\Controllers\ se carga automáticamente. Ya no necesitas require_once.

Final y readonly (PHP 8.1):

<?php
// Final: no se puede extender la clase ni sobrescribir el método
final class Configuracion
{
    // Readonly: propiedad que solo se asigna una vez (PHP 8.1)
    // Con PHP 8.2, readonly permite asignación en __clone
    public readonly string $dbUrl;
    
    public function __construct()
    {
        $this->dbUrl = getenv('DATABASE_URL') ?: 'mysql://localhost:3306/db';
    }
}

// Error: Cannot inherit from final class Configuracion
// class ConfigHija extends Configuracion {}
Fuente: PHP Manual — OOP, PHP 8.0 — Constructor Promotion RFC, PHP 8.1 — Readonly RFC

Type hints, uniones, match expression y enums

Union types, nullable y mixed:

<?php
declare(strict_types=1);

// Nullable: ?Type = Type|null
function buscar(?int $id): ?string
{
    if ($id === null) {
        return null;
    }
    return "Resultado para $id";
}

// Union type (PHP 8.0+)
function procesar(int|string $valor): string|int
{
    return match (true) {
        is_int($valor) => $valor * 2,
        is_string($valor) => strtoupper($valor),
    };
}

// Mixed (cualquier tipo, PHP 8.0+)
function debug(mixed $valor): void
{
    var_dump($valor);
}

// Never (nunca retorna, PHP 8.1+)
function abortar(string $mensaje): never
{
    echo $mensaje . PHP_EOL;
    exit(1);
}

Nullsafe operator (?->) (PHP 8.0):

<?php
// Antes (cadenas de null checks):
$ciudad = null;
if ($usuario !== null) {
    $direccion = $usuario->direccion;
    if ($direccion !== null) {
        $ciudad = $direccion->ciudad;
    }
}

// PHP 8.0+ nullsafe:
$ciudad = $usuario?->direccion?->ciudad;

// Null coalescing (??) y null coalescing assignment (??=)
$nombre = $_GET['nombre'] ?? 'Invitado';  // Si no existe, usa default
$usuario['avatar'] ??= 'default.png';     // Asigna solo si es null o no existe

match expression (PHP 8.0):

<?php
// match es una expresión (devuelve valor), no una sentencia como switch
// No necesita break, es exhaustivo (lanza UnhandledMatchError si no hay match)

$statusCode = 404;

$mensaje = match ($statusCode) {
    200, 201 => 'Éxito',
    301, 302 => 'Redirección',
    400, 422 => 'Error de validación',
    401 => 'No autenticado',
    403 => 'Prohibido',
    404 => 'No encontrado',
    500 => 'Error del servidor',
    default => 'Código desconocido',
};

// match con condiciones booleanas
$edad = 25;
$categoria = match (true) {
    $edad < 13 => 'Niño',
    $edad < 18 => 'Adolescente',
    $edad < 65 => 'Adulto',
    default => 'Senior',
};

Enums nativos (PHP 8.1):

<?php
// Enum puro (sin valor asociado)
enum Status
{
    case Draft;
    case Published;
    case Archived;
    
    // Métodos en enums
    public function label(): string
    {
        return match ($this) {
            Status::Draft => 'Borrador',
            Status::Published => 'Publicado',
            Status::Archived => 'Archivado',
        };
    }
}

// Backed enum (con valor string o int)
enum Rol: string
{
    case Admin = 'admin';
    case Editor = 'editor';
    case Viewer = 'viewer';
    
    public function permisos(): array
    {
        return match ($this) {
            Rol::Admin => ['create', 'read', 'update', 'delete'],
            Rol::Editor => ['create', 'read', 'update'],
            Rol::Viewer => ['read'],
        };
    }
}

// Uso
$rol = Rol::Admin;
echo $rol->name;      // 'Admin'
echo $rol->value;     // 'admin'
echo $rol->permisos(); // ['create', 'read', 'update', 'delete']
echo Rol::from('editor'); // Rol::Editor
echo Rol::tryFrom('xyz');  // null (no lanza excepción)

First-class callable syntax (PHP 8.1):

<?php
// Antes (PHP 8.0 y anterior):
$callback = [$this, 'metodo'];
$callback2 = 'strtoupper';

// PHP 8.1:
$callback = $this->metodo(...);
$callback2 = strtoupper(...);

// Especialmente útil con array_map/array_filter:
$nombres = ['ana', 'luis', 'pedro'];
$mayusculas = array_map(strtoupper(...), $nombres);
Fuente: PHP 8.0 — Match Expression RFC, PHP 8.1 — Enums RFC, PHP 8.1 — First-class Callable Syntax

Named arguments y attributes (PHP 8.0)

Named arguments:

<?php
// Permite pasar argumentos por nombre, no solo por posición
function crearUsuario(
    string $nombre,
    string $email,
    int $edad = 18,
    bool $activo = true,
    array $roles = ['user'],
): array {
    return compact('nombre', 'email', 'edad', 'activo', 'roles');
}

// Llamada tradicional (posicional)
$u1 = crearUsuario('Ana', 'ana@email.com', 28, true, ['admin', 'user']);

// Llamada con named arguments (solo especificas los que cambian)
$u2 = crearUsuario(
    nombre: 'Luis',
    email: 'luis@email.com',
    roles: ['editor'],
    // $edad y $activo usan valores por defecto
);

// Orden arbitrario, saltar defaults
$u3 = crearUsuario(email: 'a@b.com', nombre: 'Pe');

Attributes (PHP 8.0):

<?php
// Los atributos son metadatos estructurados para clases, métodos, propiedades
// Similar a anotaciones en Java o decoradores en Python (pero no ejecutan código)

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
class Validar
{
    public function __construct(
        public string $regla,
        public ?int $min = null,
        public ?int $max = null,
    ) {}
}

#[Attribute(Attribute::TARGET_CLASS)]
class Ruta
{
    public function __construct(
        public string $path,
        public string $method = 'GET',
    ) {}
}

// Uso
#[Ruta('/usuarios')]
class UsuarioController
{
    public function __construct(
        #[Validar('email')]
        private string $email,
        
        #[Validar('string', min: 3, max: 100)]
        private string $nombre,
    ) {}
}

// Leer atributos en runtime (reflection)
$ref = new ReflectionClass(UsuarioController::class);
$rutaAttr = $ref->getAttributes(Ruta::class)[0] ?? null;
if ($rutaAttr) {
    $ruta = $rutaAttr->newInstance();
    echo "{$ruta->method} {$ruta->path}";  // "GET /usuarios"
}

$propRef = $ref->getProperty('email');
$validarAttr = $propRef->getAttributes(Validar::class)[0]?->newInstance();
echo $validarAttr->regla;  // 'email'

Fibers (PHP 8.1) — concurrencia cooperativa:

<?php
// Fibers son corutinas a nivel de sistema: permiten pausar y reanudar
// una función en un punto específico, similar a async/await pero sin event loop

$fiber = new Fiber(function (): void {
    $valor = Fiber::suspend('retorno inicial');
    echo "Recibido: $valor";
});

$resultado = $fiber->start();
echo $resultado;  // 'retorno inicial'

$fiber->resume('mensaje desde fuera');
// Output: "Recibido: mensaje desde fuera"
Fuente: PHP 8.0 — Named Arguments RFC, PHP 8.0 — Attributes RFC, PHP 8.1 — Fibers RFC

Excepciones: try/catch/finally y custom exceptions

<?php
declare(strict_types=1);

// Excepción personalizada
class UsuarioNoEncontrado extends \RuntimeException
{
    public function __construct(
        int $id,
        string $message = '',
        int $code = 404,
        ?\Throwable $previous = null,
    ) {
        if ($message === '') {
            $message = "Usuario con ID {$id} no encontrado";
        }
        parent::__construct($message, $code, $previous);
    }
}

// Excepción de validación
class ValidacionException extends \InvalidArgumentException
{
    /** @var array<string, string[]> */
    private array $errores;
    
    public function __construct(array $errores)
    {
        parent::__construct('Error de validación', 422);
        $this->errores = $errores;
    }
    
    public function errores(): array
    {
        return $this->errores;
    }
}

// Throw expression (PHP 8.0+): throw se puede usar en expresiones
$nombre = $_GET['nombre'] ?? throw new \InvalidArgumentException('Nombre requerido');

// Uso completo
function buscarUsuario(int $id): array
{
    // Simulación de búsqueda
    if ($id <= 0) {
        throw new \InvalidArgumentException('ID inválido');
    }
    
    if ($id !== 1) {
        throw new UsuarioNoEncontrado($id);
    }
    
    return ['id' => 1, 'nombre' => 'Ana'];
}

// Try/catch/finally
try {
    $usuario = buscarUsuario(5);
    echo $usuario['nombre'];
} catch (UsuarioNoEncontrado $e) {
    echo "Error: {$e->getMessage()}";  // "Error: Usuario con ID 5 no encontrado"
    
    // try/catch anidados con throw
    throw $e;
} catch (\InvalidArgumentException $e) {
    echo "Datos inválidos: {$e->getMessage()}";
} catch (\Throwable $e) {
    // Captura cualquier error o excepción
    echo "Error inesperado: {$e->getMessage()}";
    throw $e;  // Re-lanzar si no podemos manejarlo
} finally {
    // Siempre se ejecuta, haya o no excepción
    echo "Limpiando recursos...";
}
Fuente: PHP Manual — Exceptions

Genéricos (no nativos, con PHPDoc/Psalm/PHPStan)

PHP no tiene genéricos nativos en runtime (a diferencia de Java, C# o TypeScript). Sin embargo, las herramientas de análisis estático como PHPStan (nivel max) y Psalm soportan genéricos en PHPDoc:

<?php
/**
 * @template T
 * @implements \IteratorAggregate<int, T>
 */
class Coleccion implements \IteratorAggregate
{
    /** @var array<int, T> */
    private array $items = [];
    
    /**
     * @param T $item
     */
    public function añadir(mixed $item): void
    {
        $this->items[] = $item;
    }
    
    /**
     * @return T|null
     */
    public function primero(): mixed
    {
        return $this->items[0] ?? null;
    }
    
    /**
     * @return \ArrayIterator<int, T>
     */
    public function getIterator(): \ArrayIterator
    {
        return new \ArrayIterator($this->items);
    }
}

/**
 * @template TEntity
 */
interface Repositorio
{
    /**
     * @param int $id
     * @return TEntity|null
     */
    public function find(int $id): ?object;
    
    /**
     * @return Coleccion<TEntity>
     */
    public function findAll(): Coleccion;
}

/**
 * @implements Repositorio<Usuario>
 */
class UsuarioRepository implements Repositorio
{
    public function find(int $id): ?Usuario
    {
        // PHPStan sabe que retorna Usuario|null
    }
    
    public function findAll(): Coleccion
    {
        // PHPStan sabe que retorna Coleccion<Usuario>
    }
}

Para PHP 8.3+ no hay planes concretos para genéricos en runtime. La comunidad usa PHPDoc + PHPStan/Psalm.

Fuente: PHPStan — Generics, Psalm — Template

Composer: Packagist, autoload y scripts

Composer es el gestor de dependencias de PHP. Análogo a npm (Node), pip (Python), cargo (Rust).

Packagist — el repositorio de paquetes:

packagist.org es el registro oficial, equivalente a PyPI (Python) o npmjs.com. Alberga >400,000 paquetes.

{
    "require": {
        "laravel/framework": "^11.0",
        "monolog/monolog": "^3.0",
        "guzzlehttp/guzzle": "^7.0"
    }
}

Autoload PSR-4:

{
    "autoload": {
        "psr-4": {
            "App\\": "src/",
            "App\\Domain\\": "src/Domain/",
            "App\\Infrastructure\\": "src/Infrastructure/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "Tests\\": "tests/"
        }
    }
}

Scripts de Composer:

{
    "scripts": {
        "dev": "php -S localhost:8000 -t public/",
        "test": "phpunit",
        "test:coverage": "phpunit --coverage-html coverage/",
        "phpstan": "phpstan analyse src --level=max",
        "psalm": "psalm",
        "lint": "pint",
        "fix": "pint --preset=laravel",
        "check": [
            "@lint",
            "@phpstan",
            "@test"
        ],
        "post-create-project-cmd": [
            "php artisan key:generate"
        ]
    }
}

Comandos esenciales:

ComandoEquivalente npmPara qué
composer installnpm installInstala dependencias del composer.lock
composer updatenpm updateActualiza dependencias y regenera lock
composer require Xnpm install XAñade dependencia
composer remove Xnpm uninstall XElimina dependencia
composer dump-autoload -o—Regenera autoloader (optimizado)
composer shownpm listLista dependencias instaladas
Fuente: Composer docs, Packagist

Generadores (yield) e iteradores SPL

<?php
// Generador: función que produce múltiples valores sin crear un array completo
// Similar a los generadores de Python

function numerosPares(int $limite): \Generator
{
    for ($i = 0; $i <= $limite; $i += 2) {
        // yield pausa la ejecución y retorna el valor
        // La próxima llamada a next() continúa desde aquí
        yield $i;
    }
}

foreach (numerosPares(10) as $num) {
    echo "$num ";  // 0 2 4 6 8 10
}

// Generador con claves (yield key => value)
function mapeoLetras(): \Generator
{
    yield 'a' => 1;
    yield 'b' => 2;
    yield 'c' => 3;
}

// Generador delegado (yield from)
function combinado(): \Generator
{
    yield 1;
    yield from numerosPares(6);  // 0, 2, 4, 6
    yield 99;
}

// Generadores para leer archivos grandes (memoria constante)
function leerPorLineas(string $path): \Generator
{
    $handle = fopen($path, 'r');
    while (($line = fgets($handle)) !== false) {
        yield trim($line);
    }
    fclose($handle);
}

// SPL Iterators
$iterator = new \DirectoryIterator(__DIR__);
foreach ($iterator as $file) {
    if ($file->isFile()) {
        echo $file->getFilename() . PHP_EOL;
    }
}

// Composición de iteradores
$arr = [1, 2, 3, 4, 5];
$filter = new \CallbackFilterIterator(
    new \ArrayIterator($arr),
    fn($item) => $item % 2 === 0,
);
foreach ($filter as $item) {
    echo $item;  // 2, 4
}
Fuente: PHP Manual — Generators, PHP Manual — SPL Iterators

Sistema de archivos

Operaciones básicas

¿Qué es?

PHP tiene un sistema de archivos muy completo, heredado de C y ampliado con wrappers de stream. Ofrece funciones para leer, escribir, copiar, mover, eliminar y recorrer archivos y directorios.

🔗 PHP Manual — Filesystem

Sintaxis básica

<?php
$contenido = file_get_contents('archivo.txt');
file_put_contents('copia.txt', $contenido);

$lineas = file('datos.csv');
foreach ($lineas as $linea) {
    echo trim($linea);
}

$handle = fopen('archivo.txt', 'r');
while (($line = fgets($handle)) !== false) {
    echo $line;
}
fclose($handle);

echo file_exists('archivo.txt');
echo is_file('archivo.txt');
echo is_dir('uploads');
echo filesize('archivo.txt');
echo filemtime('archivo.txt');

mkdir('nueva-carpeta', 0755, true);
rmdir('nueva-carpeta');
$archivos = scandir('uploads');
$glob = glob('uploads/*.jpg');

$info = new SplFileInfo('archivo.txt');
echo $info->getFilename();
echo $info->getExtension();
echo $info->getSize();

$file = new SplFileObject('datos.csv');
$file->setFlags(SplFileObject::READ_CSV);
foreach ($file as $row) {
    print_r($row);
}
FunciónDescripción
file_get_contents($path)Lee todo el archivo en un string
file_put_contents($path, $data)Escribe un string en archivo
fopen() / fgets() / fclose()Lectura línea por línea para archivos grandes
scandir() / glob()Listar archivos en directorio
mkdir() / rmdir()Crear/eliminar directorios
SplFileObjectOOP para recorrer archivos (especialmente CSV)

🧪 Cómo probarlo

php -r '
file_put_contents("test.txt", "Hola Mundo");
echo file_get_contents("test.txt");
unlink("test.txt");
'

💡 Memoria y rendimiento

  • file_get_contents() carga todo en memoria — para archivos >100MB usa fopen() + fgets() o SplFileObject
  • file() también carga todo en un array — igual que file_get_contents pero divide en líneas
  • glob() escanea el disco — para búsquedas repetitivas cachea resultados
  • SplFileObject con READ_CSV es eficiente para archivos CSV grandes

✅ Buenas prácticas

  • ✅ Siempre verifica file_exists() antes de leer
  • ✅ Usa LOCK_EX en file_put_contents() para escritura concurrente
  • ✅ Cierra handles con fclose() — PHP lo hace al finalizar el request pero no confíes en ello
  • ⚠️ Los paths relativos dependen del CWD — usa __DIR__ para paths absolutos
  • ❌ No confíes en $_FILES['file']['type'] — valida siempre el MIME real con finfo

🏗️ Metodología

Para archivos pequeños (&lt;10MB), file_get_contents() + file_put_contents() es suficiente. Para archivos grandes (&gt;100MB), procesa línea por línea. Para archivos remotos, usa streams con contexto (stream_context_create). Para archivos compartidos entre requests, considera usar una BD o Redis.

🔗 Para saber más

Formatos estructurados

¿Qué es?

PHP tiene soporte nativo para JSON, CSV, ZIP, GZIP, y mediante extensiones para YAML, XML y más formatos. JSON es el más usado para APIs y almacenamiento.

🔗 PHP Manual — JSON

Sintaxis básica

<?php
$data = ['nombre' => 'Ana', 'edad' => 28];

// JSON
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
file_put_contents('data.json', $json);
$decoded = json_decode(file_get_contents('data.json'), true);

// CSV
$handle = fopen('datos.csv', 'w');
fputcsv($handle, ['Nombre', 'Edad']);
fputcsv($handle, ['Ana', 28]);
fclose($handle);

$handle = fopen('datos.csv', 'r');
while ($row = fgetcsv($handle)) {
    print_r($row);
}
fclose($handle);

// ZIP
$zip = new ZipArchive();
if ($zip->open('archive.zip', ZipArchive::CREATE) === true) {
    $zip->addFile('archivo.txt', 'carpeta/archivo.txt');
    $zip->addFromString('nota.txt', 'Contenido');
    $zip->close();
}

// GZIP
$data = gzencode($contenido, 9);
file_put_contents('datos.gz', $data);
$original = gzdecode(file_get_contents('datos.gz'));

// MIME
$finfo = new finfo(FILEINFO_MIME_TYPE);
echo $finfo->file('imagen.jpg');

🧪 Cómo probarlo

php -r '
echo json_encode(["nombre" => "Ana"]);
echo json_last_error_msg();
'

💡 Memoria y rendimiento

  • json_decode() sin true (objeto stdClass) es más rápido que con true (array)
  • CSV con fgetcsv() es eficiente para archivos grandes — no carga todo en memoria
  • ZIP en compresión almacenada (store) es rápido pero no reduce tamaño
  • GZIP nivel 9 es el máximo comprimido pero más lento — nivel 6 es el balance óptimo

✅ Buenas prácticas

  • ✅ Siempre verifica json_last_error() después de json_decode()
  • ✅ Usa JSON_THROW_ON_ERROR (PHP 7.3+) para excepciones
  • ✅ Para CSV con caracteres UTF-8, añade BOM al inicio: "\xEF\xBB\xBF"
  • ⚠️ json_decode() devuelve null para JSON inválido — no se puede distinguir del string “null”
  • ❌ No uses serialize() para datos que necesites leer desde otros lenguajes

🏗️ Metodología

JSON para APIs y config. CSV para import/export con hojas de cálculo. ZIP para backups y descargas múltiples. GZIP para logs comprimidos. Para config YAML, usa la extensión yaml o Symfony YAML component.

🔗 Para saber más


Algoritmos

Ordenación

¿Qué es?

PHP tiene funciones de ordenación que modifican el array original (sort, rsort, asort, ksort) y variantes con callback (usort, uasort, uksort). Internamente usan Quicksort.

🔗 PHP Manual — Array Sorting

Sintaxis básica

<?php
$nums = [5, 3, 8, 1, 9, 2];

sort($nums);     // [1, 2, 3, 5, 8, 9]
rsort($nums);    // [9, 8, 5, 3, 2, 1]

$user = ['edad' => 28, 'nombre' => 'Ana', 'id' => 1];
asort($user);    // ordena por valor, mantiene keys
ksort($user);    // ordena por key
natsort($user);  // orden natural

usort($nums, fn($a, $b) => $b <=> $a);
uasort($user, fn($a, $b) => $a <=> $b);
uksort($user, fn($a, $b) => $a <=> $b);

shuffle($nums);
$clave = array_rand($nums);

🧪 Cómo probarlo

php -r '
$nums = [5, 3, 8, 1];
sort($nums);
var_dump($nums === [1, 3, 5, 8]);
'

💡 Memoria y rendimiento

Algoritmo internoComplejidad
sort/rsortO(n log n) Quicksort
usort/uasortO(n log n)
natsortO(n log n)

Todos modifican el array original — no crean copia. Para arrays grandes (>100k), son eficientes pero consumen memoria extra para la recursión.

✅ Buenas prácticas

  • ✅ Usa el spaceship <=> para ordenación ascendente y $b <=> $a para descendente
  • ✅ Siempre pasa SORT_NUMERIC o SORT_STRING como flag para rendimiento
  • ⚠️ $array = sort($array) asigna true (sort devuelve bool) — error común
  • ❌ No uses shuffle() si necesitas reproducibilidad — pasa srand() primero

🏗️ Metodología

Para arrays pequeños (&lt;1000), cualquier función sirve. Para grandes volúmenes, prefiere sort() sobre usort(). Si necesitas ordenación estable, usa array_multisort(). Para ordenación natural de strings (img1.jpg, img2.jpg, img10.jpg), usa natsort().

🔗 Para saber más

Búsqueda y comparación

¿Qué es?

Funciones para buscar elementos en arrays: in_array(), array_search(), array_key_exists(). También para comparar arrays: array_diff(), array_intersect(), array_unique().

<?php
$nums = [1, 2, 3, 4, 5];

$pos = array_search(3, $nums);           // 2
$existe = in_array(3, $nums);            // true
$tieneKey = array_key_exists('edad', $user);

$unicos = array_unique([1, 2, 2, 3, 3]);

$intersect = array_intersect([1, 2, 3], [2, 3, 4]);
$diff = array_diff([1, 2, 3], [2, 3, 4]);

💡 Memoria y rendimiento

  • in_array() y array_search() son O(n) — búsqueda lineal
  • isset($arr[$key]) es O(1) — mucho más rápido para verificar existencia
  • array_unique() es O(n) internamente
  • array_intersect() / array_diff() usan tablas hash — O(n + m)

✅ Buenas prácticas

  • ✅ Para verificar si una clave existe: isset($arr[$key]) o array_key_exists()
  • ⚠️ in_array() con true (strict mode) compara tipos — más seguro
  • ✅ En arrays grandes, convierte a SplFixedArray para acceso más rápido

Transformación

¿Qué es?

array_map(), array_filter() y array_reduce() son las funciones de orden superior (map/filter/reduce) que permiten transformar colecciones sin bucles explícitos.

<?php
$nums = [1, 2, 3, 4, 5];
$users = [
    ['name' => 'Ana', 'edad' => 28],
    ['name' => 'Luis', 'edad' => 35],
];

$cuadrados = array_map(fn($n) => $n ** 2, $nums);
$pares = array_filter($nums, fn($n) => $n % 2 === 0);
$suma = array_reduce($nums, fn($c, $n) => $c + $n, 0);

array_walk($nums, fn(&$n) => $n *= 2);

$nombres = array_column($users, 'name');

$chunks = array_chunk($nums, 2);
$slice = array_slice($nums, 0, 3);
array_splice($nums, 1, 2, ['a', 'b']);

$frecuencias = array_count_values([1, 1, 2, 3, 1]);

💡 Memoria y rendimiento

OperaciónComplejidad
array_map()O(n)
array_filter()O(n)
array_reduce()O(n)
array_column()O(n)

array_map() preserva keys numéricas pero reindexa las asociativas — usa array_map(null, $keys, $values) para conservar ambas.

✅ Buenas prácticas

  • ✅ array_map + arrow functions para transformaciones simples
  • ✅ array_filter sin callback elimina valores falsy: array_filter([0, 1, null, 2]) → [1, 2]
  • ⚠️ compact() es útil pero crea variables implícitas — difícil de debuggear
  • ❌ extract() está deprecado moralmente — nunca lo uses con datos del usuario

SPL — Estructuras de datos

¿Qué es?

SPL (Standard PHP Library) provee estructuras de datos eficientes: pilas (SplStack), colas (SplQueue), heaps (SplMaxHeap, SplMinHeap, SplPriorityQueue), arrays fijos (SplFixedArray), y más.

<?php
$stack = new SplStack();
$stack->push('a');
$stack->push('b');
echo $stack->pop();  // 'b'

$queue = new SplQueue();
$queue->enqueue('a');
$queue->enqueue('b');
echo $queue->dequeue();  // 'a'

$heap = new SplMaxHeap();
$heap->insert(10);
$heap->insert(5);
$heap->insert(20);
echo $heap->extract();  // 20

$pq = new SplPriorityQueue();
$pq->insert('baja', 1);
$pq->insert('alta', 10);
echo $pq->extract();  // 'alta'

$list = new SplDoublyLinkedList();
$list->push('a');
$list->push('b');
echo $list->shift();  // 'a'

💡 Memoria y rendimiento

EstructuraInternamente
SplStackLista doblemente enlazada
SplQueueLista doblemente enlazada
SplMaxHeapHeap binario
SplFixedArrayArray C nativo (más rápido que array PHP)
SplObjectStorageMapa de objetos

SplFixedArray es ~30% más rápido que array para acceso indexado y ocupa menos memoria. Útil para grandes matrices de tamaño fijo.

✅ Buenas prácticas

  • ✅ Usa SplPriorityQueue en lugar de usort() + array cuando necesites extraer ordenadamente
  • ✅ SplObjectStorage es ideal para asociar metadatos a objetos
  • ⚠️ SplFixedArray no tiene [] para añadir — usa $fa[$i] = $val con índice conocido

Tabla de complejidad Big O

OperaciónArray nativoSplFixedArraySplHeap
AccesoO(1)O(1)—
InserciónO(1) amortizadoO(1)O(log n)
BúsquedaO(n)O(n)—
sort()O(n log n)——
Fuente: PHP Manual — Array Functions, PHP Manual — SPL Data Structures

Ejemplo completo de POO en PHP

Este ejemplo une todos los conceptos de POO explicados arriba en un sistema realista de biblioteca multimedia. Léelo de arriba a abajo: cada clase añade una capa.

<?php
declare(strict_types=1);

namespace App\Media;

// ── Enum y DTOs (modelos de datos) ──

enum VideoFormat: string
{
    case MP4 = 'mp4';
    case WEBM = 'webm';
    case AVI = 'avi';
    case MKV = 'mkv';
}

enum MediaType: string
{
    case Audio = 'audio';
    case Video = 'video';
    case Podcast = 'podcast';
}

class Metadata
{
    public function __construct(
        public readonly string $title,
        public readonly int $durationSeconds,
        public readonly \DateTimeImmutable $createdAt = new \DateTimeImmutable(),
        public array $tags = [],
    ) {}

    public function formattedDuration(): string
    {
        $minutes = intdiv($this->durationSeconds, 60);
        $seconds = $this->durationSeconds % 60;
        return sprintf('%d:%02d', $minutes, $seconds);
    }

    public function __toString(): string
    {
        return sprintf('%s (%s)', $this->title, $this->formattedDuration());
    }
}

// ── Interfaz base ──

interface ContentInterface
{
    public function metadata(): Metadata;
    public function play(): string;
    public function info(): string;
    public function plays(): int;
}

// ── Trait reutilizable ──

trait Playable
{
    private int $plays = 0;

    public function play(): string
    {
        $this->plays++;
        return sprintf('▶️ Reproduciendo %s...', $this->metadata()->title);
    }

    public function plays(): int
    {
        return $this->plays;
    }
}

// ── Clase abstracta base ──

abstract class Content implements ContentInterface
{
    use Playable;

    public function __construct(
        protected Metadata $metadata,
    ) {}

    public function metadata(): Metadata
    {
        return $this->metadata;
    }

    abstract protected function typeLabel(): string;

    public function __toString(): string
    {
        return sprintf('[%s] %s', (new \ReflectionClass($this))->getShortName(), $this->metadata);
    }
}

// ── Herencia simple ──

class Audio extends Content
{
    public function __construct(
        Metadata $metadata,
        public readonly int $bitrateKbps,
    ) {
        parent::__construct($metadata);
    }

    public function info(): string
    {
        return sprintf(
            '🎵 %s | %s | %dkbps | %d reproducciones',
            $this->metadata->title,
            $this->metadata->formattedDuration(),
            $this->bitrateKbps,
            $this->plays,
        );
    }

    protected function typeLabel(): string
    {
        return 'Audio';
    }
}

class Video extends Content
{
    private bool $subtitles = false;

    public function __construct(
        Metadata $metadata,
        public readonly string $resolution,
        public readonly VideoFormat $format,
    ) {
        parent::__construct($metadata);
    }

    public function hasSubtitles(): bool
    {
        return $this->subtitles;
    }

    public function enableSubtitles(): static
    {
        $this->subtitles = true;
        return $this;
    }

    public function disableSubtitles(): static
    {
        $this->subtitles = false;
        return $this;
    }

    public function info(): string
    {
        return sprintf(
            '🎬 %s | %s | %s | %s',
            $this->metadata->title,
            $this->resolution,
            $this->format->value,
            $this->subtitles ? 'CC' : 'Sin subtítulos',
        );
    }

    protected function typeLabel(): string
    {
        return 'Video';
    }
}

// ── Interfaz adicional y herencia múltiple con traits ──

interface Exportable
{
    public function toArray(): array;
    public function toJson(): string;
}

trait JsonExportable
{
    public function toJson(): string
    {
        return json_encode($this->toArray(), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
    }
}

class Podcast extends Audio implements Exportable
{
    use JsonExportable;

    public function __construct(
        Metadata $metadata,
        int $bitrateKbps,
        public readonly int $episode,
        public readonly string $host,
    ) {
        parent::__construct($metadata, $bitrateKbps);
    }

    public function info(): string
    {
        return sprintf(
            '🎙️ Ep.%d: %s | con %s | %s',
            $this->episode,
            $this->metadata->title,
            $this->host,
            parent::info(),
        );
    }

    public function toArray(): array
    {
        return [
            'type' => 'podcast',
            'episode' => $this->episode,
            'host' => $this->host,
            'title' => $this->metadata->title,
            'duration' => $this->metadata->formattedDuration(),
            'bitrate' => $this->bitrateKbps,
            'plays' => $this->plays,
            'tags' => $this->metadata->tags,
        ];
    }

    protected function typeLabel(): string
    {
        return 'Podcast';
    }
}

// ── Composición: un objeto contiene otros objetos ──

class Playlist implements \IteratorAggregate, \Countable
{
    /** @var Content[] */
    private array $items = [];

    public function __construct(
        public readonly string $name,
    ) {}

    public function add(Content $item): void
    {
        $this->items[] = $item;
    }

    public function remove(int $index): void
    {
        if (isset($this->items[$index])) {
            array_splice($this->items, $index, 1);
        }
    }

    /** @return string[] */
    public function playAll(): array
    {
        return array_map(fn(Content $c) => $c->play(), $this->items);
    }

    /** @return string[] */
    public function allInfo(): array
    {
        return array_map(fn(Content $c) => $c->info(), $this->items);
    }

    public function count(): int
    {
        return count($this->items);
    }

    public function getIterator(): \ArrayIterator
    {
        return new \ArrayIterator($this->items);
    }

    public function __toString(): string
    {
        return sprintf('📋 %s (%d items)', $this->name, count($this->items));
    }
}

// ── Uso del sistema ──

// Crear contenido de distintos tipos
$song = new Audio(
    new Metadata('Bohemian Rhapsody', 354, tags: ['rock', 'clásico']),
    bitrateKbps: 320,
);

$movie = new Video(
    new Metadata('Inception', 8880, tags: ['ciencia ficción', 'thriller']),
    resolution: '4K',
    format: VideoFormat::MKV,
);
$movie->enableSubtitles();

$podcast = new Podcast(
    new Metadata('El futuro de la IA', 2700, tags: ['tecnología', 'IA']),
    bitrateKbps: 128,
    episode: 42,
    host: 'Ana López',
);

// Polimorfismo: cada uno implementa info() a su manera
foreach ([$song, $movie, $podcast] as $content) {
    echo $content->info() . PHP_EOL;
}

// Composición
$playlist = new Playlist('Favoritos');
$playlist->add($song);
$playlist->add($movie);
$playlist->add($podcast);
echo $playlist . PHP_EOL;  // 📋 Favoritos (3 items)
print_r($playlist->playAll());
print_r($playlist->allInfo());

// Exportación vía interfaz + trait
echo $podcast->toJson() . PHP_EOL;

// Countable + IteratorAggregate
echo count($playlist) . PHP_EOL;  // 3
foreach ($playlist as $item) {
    echo "  - {$item->metadata()->title}" . PHP_EOL;
}

Conceptos demostrados:

ConceptoCómo aparece
Clase abstractaContent con método abstracto typeLabel()
Herencia simpleAudio(Content), Video(Content)
InterfacesContentInterface, Exportable — definen contratos
TraitsPlayable y JsonExportable — reutilización horizontal
parent::parent::__construct() y parent::info() en Podcast
EncapsulaciónPropiedades private/protected, métodos públicos
Polimorfismoinfo() da resultados distintos según la clase concreta
ComposiciónPlaylist contiene Content[]
EnumsVideoFormat, MediaType con métodos
Constructor promotionpublic readonly string $title sin propiedades separadas
ReadonlyMetadata usa readonly para inmutabilidad
Type hintsTipos estrictos en todos los métodos
GeneradoresgetIterator() con ArrayIterator
SPLArrayIterator, Countable, IteratorAggregate

Patrones de diseño con ejemplos

Strategy

Permite intercambiar algoritmos en tiempo de ejecución. En PHP puedes usar tanto interfaces como funciones anónimas/callables.

<?php
declare(strict_types=1);

// Estrategia como interfaz
interface SortStrategy
{
    /** @param int[] $data */
    public function sort(array $data): array;
}

class AscendingSort implements SortStrategy
{
    public function sort(array $data): array
    {
        sort($data);
        return $data;
    }
}

class DescendingSort implements SortStrategy
{
    public function sort(array $data): array
    {
        rsort($data);
        return $data;
    }
}

class OddFirstSort implements SortStrategy
{
    public function sort(array $data): array
    {
        $odds = array_filter($data, fn($n) => $n % 2 !== 0);
        $evens = array_filter($data, fn($n) => $n % 2 === 0);
        return [...$odds, ...$evens];
    }
}

// Contexto
class Analyzer
{
    public function __construct(
        private SortStrategy $strategy,
    ) {}

    public function analyze(array $data): array
    {
        return $this->strategy->sort($data);
    }
}

// Uso
$analyzer = new Analyzer(new DescendingSort());
print_r($analyzer->analyze([3, 1, 4, 1, 5, 9]));

// Alternativa: estrategia como callable (más simple)
class AnalyzerCallable
{
    /** @param callable(int[]): int[] $strategy */
    public function __construct(
        private callable $strategy,
    ) {}

    public function analyze(array $data): array
    {
        return ($this->strategy)($data);
    }
}

$customSort = new AnalyzerCallable(
    fn(array $d) => array_reverse($d),
);

Observer (SplSubject / SplObserver)

PHP tiene interfaces SPL nativas para el patrón Observer.

<?php
declare(strict_types=1);

class Sensor implements \SplSubject
{
    private int $state = 0;

    /** @var \SplObserver[] */
    private array $observers = [];

    public function attach(\SplObserver $observer): void
    {
        $this->observers[] = $observer;
    }

    public function detach(\SplObserver $observer): void
    {
        $this->observers = array_filter(
            $this->observers,
            fn(\SplObserver $o) => $o !== $observer,
        );
    }

    public function notify(): void
    {
        foreach ($this->observers as $observer) {
            $observer->update($this);
        }
    }

    public function readSensor(): int
    {
        return $this->state;
    }

    public function setState(int $value): void
    {
        $this->state = $value;
        $this->notify();
    }
}

class LoggerObserver implements \SplObserver
{
    public function update(\SplSubject $subject): void
    {
        if ($subject instanceof Sensor) {
            echo sprintf("[LOG] Sensor actualizado: %d\n", $subject->readSensor());
        }
    }
}

class DashboardObserver implements \SplObserver
{
    public function update(\SplSubject $subject): void
    {
        if ($subject instanceof Sensor) {
            echo sprintf("[DASH] Gráfico actualizado con valor %d\n", $subject->readSensor());
        }
    }
}

// Uso
$sensor = new Sensor();
$sensor->attach(new LoggerObserver());
$sensor->attach(new DashboardObserver());
$sensor->setState(42);
// [LOG] Sensor actualizado: 42
// [DASH] Gráfico actualizado con valor 42
Fuente: PHP Manual — SplSubject, PHP Manual — SplObserver

Singleton (el ant-patrón; mejor usa DI)

<?php
// Singleton clásico (NO recomendado en proyectos modernos)
class Database
{
    private static ?self $instance = null;
    private \PDO $pdo;
    
    private function __construct()
    {
        $this->pdo = new \PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
    }
    
    public static function getInstance(): self
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }
    
    // Evitar clonación y deserialización
    private function __clone() {}
    public function __wakeup(): void
    {
        throw new \RuntimeException('No se puede deserializar un singleton');
    }
    
    public function connection(): \PDO
    {
        return $this->pdo;
    }
}

// MEJOR: Inyección de dependencias (DI Container)
// Laravel: app()->make(Database::class) siempre devuelve la misma instancia
// Symfony: autowiring + shared: true
// Sin framework: usa un simple DI container
class Container
{
    /** @var array<string, object> */
    private array $instances = [];
    
    public function singleton(string $class, callable $factory): void
    {
        $this->instances[$class] = $factory($this);
    }
    
    public function get(string $class): object
    {
        if (!isset($this->instances[$class])) {
            throw new \RuntimeException("No binding for {$class}");
        }
        return $this->instances[$class];
    }
}

// Uso del container
$container = new Container();
$container->singleton(Database::class, fn() => new Database());
$db = $container->get(Database::class);  // Siempre la misma instancia

Factory (con registro dinámico)

<?php
declare(strict_types=1);

interface PaymentGateway
{
    public function charge(float $amount, array $metadata): array;
}

class StripeGateway implements PaymentGateway
{
    public function charge(float $amount, array $metadata): array
    {
        return ['provider' => 'stripe', 'amount' => $amount, 'status' => 'success'];
    }
}

class PayPalGateway implements PaymentGateway
{
    public function charge(float $amount, array $metadata): array
    {
        return ['provider' => 'paypal', 'amount' => $amount, 'status' => 'success'];
    }
}

class PaymentFactory
{
    /** @var array<string, class-string<PaymentGateway>> */
    private static array $gateways = [
        'stripe' => StripeGateway::class,
        'paypal' => PayPalGateway::class,
    ];
    
    public static function register(string $name, string $gatewayClass): void
    {
        self::$gateways[$name] = $gatewayClass;
    }
    
    public static function create(string $name): PaymentGateway
    {
        if (!isset(self::$gateways[$name])) {
            throw new \InvalidArgumentException(
                "Gateway '{$name}' no registrado. Disponibles: " . implode(', ', array_keys(self::$gateways))
            );
        }
        
        $class = self::$gateways[$name];
        return new $class();
    }
}

// Uso
$gateway = PaymentFactory::create('stripe');
echo $gateway->charge(100.0, ['order_id' => '123'])['status'];  // 'success'

// Registrar nuevo gateway dinámicamente
PaymentFactory::register('mercadopago', MercadoPagoGateway::class);

Builder (fluent interface)

<?php
declare(strict_types=1);

class QueryBuilder
{
    private string $table = '';
    /** @var string[] */
    private array $selects = ['*'];
    /** @var array<string, mixed> */
    private array $wheres = [];
    private ?int $limit = null;
    private ?int $offset = null;
    /** @var string[] */
    private array $orders = [];

    public function table(string $table): static
    {
        $this->table = $table;
        return $this;
    }

    public function select(string ...$columns): static
    {
        $this->selects = $columns;
        return $this;
    }

    public function where(string $column, mixed $value): static
    {
        $this->wheres[$column] = $value;
        return $this;
    }

    public function limit(int $limit): static
    {
        $this->limit = $limit;
        return $this;
    }

    public function offset(int $offset): static
    {
        $this->offset = $offset;
        return $this;
    }

    public function orderBy(string $column, string $direction = 'ASC'): static
    {
        $this->orders[] = "{$column} {$direction}";
        return $this;
    }

    public function build(): string
    {
        $sql = 'SELECT ' . implode(', ', $this->selects) . ' FROM ' . $this->table;

        if (!empty($this->wheres)) {
            $conditions = array_map(
                fn(string $col, mixed $val) => is_string($val)
                    ? "{$col} = '{$val}'"
                    : "{$col} = {$val}",
                array_keys($this->wheres),
                $this->wheres,
            );
            $sql .= ' WHERE ' . implode(' AND ', $conditions);
        }

        if (!empty($this->orders)) {
            $sql .= ' ORDER BY ' . implode(', ', $this->orders);
        }

        if ($this->limit !== null) {
            $sql .= " LIMIT {$this->limit}";
        }

        if ($this->offset !== null) {
            $sql .= " OFFSET {$this->offset}";
        }

        return $sql;
    }
}

// Uso
$query = (new QueryBuilder())
    ->table('usuarios')
    ->select('id', 'nombre', 'email')
    ->where('activo', 1)
    ->where('rol', 'admin')
    ->orderBy('nombre')
    ->limit(10)
    ->offset(0)
    ->build();

// "SELECT id, nombre, email FROM usuarios WHERE activo = 1 AND rol = 'admin' ORDER BY nombre ASC LIMIT 10 OFFSET 0"

Adapter (interfaces)

<?php
declare(strict_types=1);

// Interfaz que espera nuestro sistema
interface PaymentProcessor
{
    public function pay(float $amount, string $currency): array;
}

// Sistema externo con API diferente
class OldPaymentSystem
{
    public function makePayment(float $total, string $curr): string
    {
        return "Paid {$curr} {$total}";
    }
}

// Adapter: unifica la interfaz
class OldPaymentAdapter implements PaymentProcessor
{
    public function __construct(
        private OldPaymentSystem $system,
    ) {}

    public function pay(float $amount, string $currency): array
    {
        $result = $this->system->makePayment($amount, $currency);
        return [
            'success' => true,
            'message' => $result,
            'amount' => $amount,
            'currency' => $currency,
        ];
    }
}

// Nuevo sistema nativo
class NewPaymentSystem implements PaymentProcessor
{
    public function pay(float $amount, string $currency): array
    {
        return [
            'success' => true,
            'amount' => $amount,
            'currency' => $currency,
            'transaction_id' => uniqid('tx_'),
        ];
    }
}

// Uso: ambos se usan igual gracias al adapter
$processor = new OldPaymentAdapter(new OldPaymentSystem());
echo $processor->pay(100.0, 'USD')['message'];  // "Paid USD 100"

$processor2 = new NewPaymentSystem();
print_r($processor2->pay(50.0, 'EUR'));

Polimorfismo en detalle

PHP tiene varias formas de lograr polimorfismo. Todas coexisten en el mismo código.

1. Polimorfismo por interfaces (el más común)

<?php
declare(strict_types=1);

interface Speaker
{
    public function sound(): string;
}

class Dog implements Speaker
{
    public function sound(): string
    {
        return 'Guau';
    }
}

class Cat implements Speaker
{
    public function sound(): string
    {
        return 'Miau';
    }
}

class Car implements Speaker
{
    public function sound(): string
    {
        return 'Brum';
    }
}

function makeSound(Speaker $speaker): void
{
    echo $speaker->sound() . PHP_EOL;
}

makeSound(new Dog());  // Guau
makeSound(new Cat());  // Miau
makeSound(new Car());  // Brum (¡implementa la interfaz!)

2. Polimorfismo por herencia (abstract + override)

<?php
declare(strict_types=1);

abstract class Shape
{
    abstract public function area(): float;
}

class Circle extends Shape
{
    public function __construct(
        private float $radius,
    ) {}

    public function area(): float
    {
        return M_PI * $this->radius ** 2;
    }
}

class Rectangle extends Shape
{
    public function __construct(
        private float $width,
        private float $height,
    ) {}

    public function area(): float
    {
        return $this->width * $this->height;
    }
}

function printArea(Shape $shape): void
{
    printf("Área: %.2f\n", $shape->area());
}

printArea(new Circle(5));       // Área: 78.54
printArea(new Rectangle(3, 4)); // Área: 12.00

3. Polimorfismo por traits (reutilización con override)

<?php
declare(strict_types=1);

trait Loggable
{
    public function log(string $message): void
    {
        echo "[{$this->logPrefix()}] {$message}" . PHP_EOL;
    }

    abstract protected function logPrefix(): string;
}

class FileLogger
{
    use Loggable;

    protected function logPrefix(): string
    {
        return 'FILE';
    }
}

class DatabaseLogger
{
    use Loggable;

    protected function logPrefix(): string
    {
        return 'DB';
    }
}

$logger = new FileLogger();
$logger->log('Archivo creado');  // [FILE] Archivo creado

4. Union types + match (polimorfismo sin herencia)

<?php
declare(strict_types=1);

// Polimorfismo ad-hoc con union types (PHP 8.0+)
function processInput(string|int|float $input): string
{
    return match (true) {
        is_string($input) => "String: " . strtoupper($input),
        is_int($input) => "Int: " . ($input * 2),
        is_float($input) => "Float: " . number_format($input, 2),
    };
}

echo processInput('hola');   // "String: HOLA"
echo processInput(42);       // "Int: 84"
echo processInput(3.1416);   // "Float: 3.14"

5. Overloading con __call (método mágico)

<?php
declare(strict_types=1);

// PHP no soporta sobrecarga de métodos tradicional (varias firmas con mismo nombre)
// Pero puedes emularlo con __call
class DynamicRouter
{
    /** @var array<string, callable> */
    private array $handlers = [];

    public function __call(string $name, array $arguments): mixed
    {
        if (isset($this->handlers[$name])) {
            return ($this->handlers[$name])(...$arguments);
        }

        throw new \BadMethodCallException("Método '{$name}' no encontrado");
    }

    public function __set(string $name, callable $handler): void
    {
        $this->handlers[$name] = $handler;
    }
}

$router = new DynamicRouter();
$router->saludo = fn(string $nombre) => "Hola, {$nombre}!";
$router->despedida = fn() => "Adiós!";

echo $router->saludo('Ana');   // "Hola, Ana!"
echo $router->despedida();     // "Adiós!"

Multimedia

PHP tiene varias extensiones para procesamiento de imágenes, video y PDF.

GD (extensión nativa)

GD viene incluida con PHP y permite crear y manipular imágenes. Soporta JPEG, PNG, GIF, WebP, AVIF.

<?php
declare(strict_types=1);

$img = imagecreatetruecolor(800, 600);

$bg = imagecolorallocate($img, 255, 255, 255);
$textColor = imagecolorallocate($img, 0, 0, 0);

imagefill($img, 0, 0, $bg);

imagettftext($img, 20, 0, 50, 100, $textColor,
    '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 'Hola PHP!');

$src = imagecreatefromjpeg('foto.jpg');
$thumb = imagecreatetruecolor(200, 150);
imagecopyresampled($thumb, $src, 0, 0, 0, 0, 200, 150, imagesx($src), imagesy($src));

imagefilter($img, IMG_FILTER_GRAYSCALE);

imagejpeg($img, 'output.jpg', 90);
imagepng($img, 'output.png');
imagegif($img, 'output.gif');
imagewebp($img, 'output.webp', 80);
imageavif($img, 'output.avif');

imagedestroy($img);

Imagick (ImageMagick OO)

Imagick es mas potente que GD: mas formatos, mejor calidad, mas filtros.

pecl install imagick
<?php
$img = new Imagick('foto.jpg');
$img->resizeImage(800, 600, Imagick::FILTER_LANCZOS, 1, true);
$img->cropImage(400, 300, 100, 50);
$img->rotateImage(new ImagickPixel('none'), 90);
$img->blurImage(5, 3);
$img->sharpenImage(3, 2);

$watermark = new Imagick('watermark.png');
$img->compositeImage($watermark, Imagick::COMPOSITE_OVER, 10, 10);

$draw = new ImagickDraw();
$draw->setFontSize(30);
$draw->setGravity(Imagick::GRAVITY_CENTER);
$img->annotateImage($draw, 0, 0, 0, 'Marca de agua');

$img->setImageFormat('webp');
$img->writeImage('output.webp');
$img->clear();

FFmpeg (video)

<?php
exec("ffmpeg -i input.mp4 -vf scale=640:360 -c:v libx264 -crf 23 output_small.mp4 2>&1", $output, $exitCode);

// php-ffmpeg: composer require php-ffmpeg/php-ffmpeg
// $ffmpeg = FFMpeg\FFMpeg::create();
// $video = $ffmpeg->open('input.mp4');
// $video->filters()->resize(new FFMpeg\Coordinate\Dimension(640, 360))->synchronize();
// $video->save(new FFMpeg\Format\Video\X264(), 'output.mp4');

Metadatos de imagenes

<?php
[$width, $height, $type, $attr] = getimagesize('foto.jpg');
echo "$width x $height";
echo image_type_to_mime_type($type);

$exif = exif_read_data('foto.jpg');
echo $exif['Make'];
echo $exif['Model'];
echo $exif['ExposureTime'];
echo $exif['FNumber'];
echo $exif['ISOSpeedRatings'];
echo $exif['DateTimeOriginal'];
echo $exif['GPSLatitude'];

PDF

<?php
// TCPDF: composer require tecnickcom/tcpdf
// $pdf = new TCPDF();
// $pdf->AddPage();
// $pdf->SetFont('dejavusans', '', 14);
// $pdf->Cell(0, 10, 'Hola Mundo!', 0, 1);
// $pdf->Image('foto.jpg', 15, 30, 100);
// $pdf->Output('documento.pdf', 'D');

// FPDF: composer require setasign/fpdf
Fuente: PHP Manual - GD, Imagick docs, FFmpeg, TCPDF, PHP Manual - EXIF

Bases de datos

PDO (PHP Data Objects) — capa de abstracción estándar

PDO es la extensión nativa de PHP para acceso a bases de datos. Soporta MySQL, PostgreSQL, SQLite, SQL Server, Oracle, etc.

<?php
declare(strict_types=1);

// Conexión
$dsn = 'mysql:host=127.0.0.1;port=3306;dbname=blog;charset=utf8mb4';
$user = 'root';
$pass = 'secret';

$options = [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,         // Lanza excepciones
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,    // Array asociativo
    PDO::ATTR_EMULATE_PREPARES => false,                 // Prepared statements reales
    PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci",
];

try {
    $pdo = new PDO($dsn, $user, $pass, $options);
} catch (PDOException $e) {
    throw new RuntimeException("Conexión fallida: " . $e->getMessage());
}

// Prepared statements (nunca concatenes SQL)
$stmt = $pdo->prepare('SELECT * FROM usuarios WHERE email = :email AND activo = :activo');
$stmt->execute(['email' => 'ana@email.com', 'activo' => true]);
$usuario = $stmt->fetch();

// Insert con named params
$stmt = $pdo->prepare('INSERT INTO usuarios (nombre, email, edad) VALUES (:nombre, :email, :edad)');
$stmt->execute([
    'nombre' => 'Ana',
    'email' => 'ana@email.com',
    'edad' => 28,
]);
$id = $pdo->lastInsertId();

// Transacciones
$pdo->beginTransaction();
try {
    $pdo->exec('UPDATE cuentas SET saldo = saldo - 100 WHERE id = 1');
    $pdo->exec('UPDATE cuentas SET saldo = saldo + 100 WHERE id = 2');
    $pdo->commit();
} catch (PDOException $e) {
    $pdo->rollBack();
    throw $e;
}

// Múltiples consultas con un solo prepare
$stmt = $pdo->prepare('SELECT * FROM usuarios WHERE id = ?');
foreach ([1, 2, 3] as $id) {
    $stmt->execute([$id]);
    print_r($stmt->fetch());
}
Base de datosDSNDriver
MySQL / MariaDBmysql:host=...;dbname=...pdo_mysql
PostgreSQLpgsql:host=...;dbname=...pdo_pgsql
SQLitesqlite:/path/to/db.sqlitepdo_sqlite
SQL Serversqlsrv:Server=...;Database=...pdo_sqlsrv
Oracleoci:dbname=//localhost:1521/dbpdo_oci

Eloquent ORM (Laravel)

Eloquent es el ORM incluido en Laravel. Usa Active Record: cada modelo es una fila de la BD.

<?php
// Migración (base de datos)
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
    public function up(): void
    {
        Schema::create('usuarios', function (Blueprint $table) {
            $table->id();
            $table->string('nombre', 100);
            $table->string('email', 255)->unique();
            $table->integer('edad')->nullable();
            $table->timestamps();  // created_at, updated_at
        });
    }
    
    public function down(): void
    {
        Schema::dropIfExists('usuarios');
    }
};

// Modelo
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Usuario extends Model
{
    protected $fillable = ['nombre', 'email', 'edad'];  // Mass assignment
    
    protected $casts = [
        'edad' => 'integer',
        'created_at' => 'datetime',
    ];
    
    // Relaciones
    public function posts(): HasMany
    {
        return $this->hasMany(Post::class);
    }
    
    // Accessor
    public function getNombreMayusculasAttribute(): string
    {
        return strtoupper($this->nombre);
    }
    
    // Scope
    public function scopeMayoresDe($query, int $edad)
    {
        return $query->where('edad', '>=', $edad);
    }
}

// Uso
$usuario = Usuario::create([
    'nombre' => 'Ana',
    'email' => 'ana@email.com',
    'edad' => 28,
]);

$usuarios = Usuario::where('edad', '>=', 18)
    ->orderBy('nombre')
    ->take(10)
    ->get();

foreach ($usuarios as $u) {
    echo "{$u->nombre} ({$u->edad})";
}

// Eager loading (evita N+1 queries)
$usuarios = Usuario::with('posts')->get();
foreach ($usuarios as $u) {
    foreach ($u->posts as $post) {
        echo $post->title;
    }
}

Doctrine ORM (Symfony)

Doctrine es el ORM de Symfony. Usa Data Mapper: las entidades son POPOs (Plain Old PHP Objects) sin herencia de clase base.

<?php
// Entidad (anotaciones o attributes, PHP 8.0+)
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ORM\Table(name: 'usuarios')]
class Usuario
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: 'integer')]
    private int $id;
    
    #[ORM\Column(type: 'string', length: 100)]
    private string $nombre;
    
    #[ORM\Column(type: 'string', length: 255, unique: true)]
    private string $email;
    
    #[ORM\Column(type: 'integer', nullable: true)]
    private ?int $edad = null;
    
    // Getters y setters necesarios (Doctrine los usa)
    public function getId(): int { return $this->id; }
    public function getNombre(): string { return $this->nombre; }
    public function setNombre(string $nombre): static { $this->nombre = $nombre; return $this; }
    public function getEmail(): string { return $this->email; }
    public function setEmail(string $email): static { $this->email = $email; return $this; }
}

// Uso con EntityManager
$entityManager = /* obtenido del container */;

$usuario = new Usuario();
$usuario->setNombre('Ana')->setEmail('ana@email.com');

$entityManager->persist($usuario);
$entityManager->flush();  // INSERT

// Consultas con DQL (Doctrine Query Language)
$query = $entityManager->createQuery(
    'SELECT u FROM App\Entity\Usuario u WHERE u.edad >= :edad ORDER BY u.nombre ASC'
);
$query->setParameter('edad', 18);
$usuarios = $query->getResult();

Redis (caché y sesiones)

<?php
declare(strict_types=1);

use Predis\Client as Redis;

$redis = new Redis([
    'scheme' => 'tcp',
    'host' => '127.0.0.1',
    'port' => 6379,
]);

// String
$redis->set('usuario:1:nombre', 'Ana');
echo $redis->get('usuario:1:nombre');  // 'Ana'

// Hash
$redis->hmset('usuario:1', ['nombre' => 'Ana', 'email' => 'ana@email.com']);
echo $redis->hget('usuario:1', 'email');

// List (cola)
$redis->lpush('notificaciones', 'mensaje1');
$redis->rpush('notificaciones', 'mensaje2');
echo $redis->lpop('notificaciones');  // 'mensaje1'

// Set (conjunto)
$redis->sadd('tags:php', 'laravel', 'symfony', 'php8');
$redis->sismember('tags:php', 'laravel');  // true

// Cache con TTL
$redis->setex('usuario:1:datos', 3600, json_encode($usuarioArray));

// Rate limiting
$clave = "rate_limit:{$ip}";
$current = $redis->incr($clave);
if ($current === 1) {
    $redis->expire($clave, 60);  // Primer request, expira en 60s
}
if ($current > 100) {
    throw new \RuntimeException('Rate limit excedido');
}

Migraciones con Phinx

composer require --dev robmorgan/phinx

# Inicializar
vendor/bin/phinx init

# Crear migración
vendor/bin/phinx create CrearUsuarios

# Ejecutar
vendor/bin/phinx migrate
vendor/bin/phinx rollback
vendor/bin/phinx status
<?php
// db/migrations/20240101000000_crear_usuarios.php
use Phinx\Migration\AbstractMigration;

final class CrearUsuarios extends AbstractMigration
{
    public function change(): void
    {
        $table = $this->table('usuarios', ['id' => false, 'primary_key' => 'id']);
        $table->addColumn('id', 'integer', ['identity' => true])
              ->addColumn('nombre', 'string', ['limit' => 100])
              ->addColumn('email', 'string', ['limit' => 255])
              ->addColumn('edad', 'integer', ['null' => true])
              ->addColumn('created_at', 'datetime', ['default' => 'CURRENT_TIMESTAMP'])
              ->addIndex(['email'], ['unique' => true])
              ->create();
    }
}
Fuente: PHP Manual — PDO, Laravel Eloquent docs, Doctrine docs, Phinx docs

WebSockets

PHP tradicionalmente es síncrono (cada request → proceso → response), pero existen librerías para WebSockets y tiempo real.

Ratchet (WebSocket server)

composer require cboden/ratchet
<?php
declare(strict_types=1);

use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;

class ChatServer implements MessageComponentInterface
{
    /** @var \SplObjectStorage<ConnectionInterface, mixed> */
    private \SplObjectStorage $clients;

    public function __construct()
    {
        $this->clients = new \SplObjectStorage();
        echo "Servidor WebSocket iniciado\n";
    }

    public function onOpen(ConnectionInterface $conn): void
    {
        $this->clients->attach($conn);
        echo "Nueva conexión: {$conn->resourceId}\n";
    }

    public function onMessage(ConnectionInterface $from, $msg): void
    {
        $numRecv = count($this->clients) - 1;
        echo sprintf('Mensaje de %d a %d otros: %s', $from->resourceId, $numRecv, $msg);

        foreach ($this->clients as $client) {
            if ($from !== $client) {
                $client->send($msg);  // Broadcast
            }
        }
    }

    public function onClose(ConnectionInterface $conn): void
    {
        $this->clients->detach($conn);
        echo "Conexión cerrada: {$conn->resourceId}\n";
    }

    public function onError(ConnectionInterface $conn, \Exception $e): void
    {
        echo "Error: {$e->getMessage()}\n";
        $conn->close();
    }
}

// Iniciar servidor (ejecutar en CLI)
// php server.php
$server = IoServer::factory(
    new HttpServer(
        new WsServer(
            new ChatServer()
        )
    ),
    8080
);
$server->run();

ReactPHP (event loop + WebSocket)

<?php
declare(strict_types=1);

use React\EventLoop\Loop;
use React\Http\HttpServer;
use React\Http\Message\Response;
use Psr\Http\Message\ServerRequestInterface;

$loop = Loop::get();

// Servidor HTTP con ReactPHP
$server = new HttpServer(
    function (ServerRequestInterface $request) use ($loop) {
        return new Response(
            200,
            ['Content-Type' => 'text/plain'],
            "Hola desde ReactPHP en {$request->getUri()}"
        );
    }
);

$socket = new \React\Socket\SocketServer('0.0.0.0:8080', [], $loop);
$server->listen($socket);

echo "Servidor ReactPHP en http://localhost:8080\n";

// Timer (setInterval)
$loop->addPeriodicTimer(5.0, function () {
    echo "Tick cada 5 segundos\n";
});

$loop->run();

amphp (fiber-based async)

<?php
declare(strict_types=1);

// amphp usa Fibers (PHP 8.1+) para async nativo
use function Amp\async;
use function Amp\delay;
use function Amp\Parallel\Worker\submit;

// Corutinas con amphp
$handler = function () {
    // await no bloqueante
    echo "Inicio\n";
    delay(1.0);  // Pausa sin bloquear
    echo "Después de 1 segundo\n";
};

// Ejecutar concurrentemente
\Amp\Future\await([
    async($handler),
    async($handler),
]);

Swoole (server async en C)

Swoole es una extensión de PHP escrita en C que proporciona un servidor HTTP, WebSocket y TCP asíncrono.

<?php
declare(strict_types=1);

// Servidor HTTP asíncrono con Swoole
$http = new Swoole\Http\Server('0.0.0.0', 9501);

$http->on('request', function (Swoole\Http\Request $request, Swoole\Http\Response $response) {
    $response->header('Content-Type', 'application/json');
    $response->end(json_encode([
        'message' => 'Hola desde Swoole',
        'uri' => $request->server['request_uri'],
    ]));
});

// Servidor WebSocket
$ws = new Swoole\WebSocket\Server('0.0.0.0', 9502);

$ws->on('open', function (Swoole\WebSocket\Server $server, Swoole\Http\Request $request) {
    echo "Cliente conectado: {$request->fd}\n";
});

$ws->on('message', function (Swoole\WebSocket\Server $server, Swoole\WebSocket\Frame $frame) {
    echo "Mensaje: {$frame->data}\n";
    // Broadcast a todos
    foreach ($server->connections as $fd) {
        if ($server->isEstablished($fd)) {
            $server->push($fd, $frame->data);
        }
    }
});

$ws->on('close', function (Swoole\WebSocket\Server $server, int $fd) {
    echo "Cliente desconectado: {$fd}\n";
});

$http->start();
Fuente: Ratchet docs, ReactPHP docs, amphp docs, Swoole docs

Concurrencia

PHP es tradicionalmente síncrono y monolítico por diseño: cada request de HTTP crea un proceso que se ejecuta secuencialmente y termina. Sin embargo, hay formas de lograr concurrencia.

Tabla comparativa de modelos de concurrencia en PHP

ModeloLibreríaMecanismoPara quéAsync
Event loopReactPHPloop->addTimer(), promesasI/O-bound (HTTP, BD, archivos)Sí
Corutinas con FibersamphpAmp\async(), Fibers PHP 8.1I/O-bound, concurrencia estructuradaSí
Corutinas en CSwoolego(), chan(), servidor propioAlto rendimiento, reemplazo de FPMSí
Extension parallelparallel (||)Hilos reales (OS threads)CPU-bound en paralelo realSí (bloqueante)
Multiprocesamientopcntl_fork()Procesos hijosAislamiento total, tareas largasNo
ColasLaravel Horizon, RabbitMQWorkers en backgroundTareas diferidas, emails, notificacionesNo
Server-sent eventsN/AEvent loop del navegadorTiempo real unidireccionalSí (navegador)

ReactPHP (event loop + promises)

<?php
declare(strict_types=1);

use React\EventLoop\Loop;
use React\Http\Browser;
use function React\Async\async;

$loop = Loop::get();

$client = new Browser();

$urls = [
    'https://api.github.com/repos/php/php-src',
    'https://api.github.com/repos/laravel/laravel',
    'https://api.github.com/repos/symfony/symfony',
];

foreach ($urls as $url) {
    $client->get($url)->then(
        function (Psr\Http\Message\ResponseInterface $response) use ($url) {
            $data = json_decode((string) $response->getBody(), true);
            echo "[{$data['full_name']}] Stars: {$data['stargazers_count']}\n";
        },
        function (Exception $e) use ($url) {
            echo "Error en {$url}: {$e->getMessage()}\n";
        },
    );
}

$loop->run();

amphp (Fibers + concurrencia estructurada)

<?php
declare(strict_types=1);

// amphp usa Fibers nativos de PHP 8.1 para async
// No necesita extensiones C, funciona con PHP puro

use function Amp\async;
use function Amp\delay;
use Amp\Http\Client\HttpClientBuilder;
use Amp\Http\Client\Request;

// Concurrencia con amphp
$client = HttpClientBuilder::buildDefault();

function fetchUrl(HttpClientBuilder $client, string $url): array
{
    $request = new Request($url);
    $response = $client->request($request);
    $body = $response->getBody()->buffer();
    
    return [
        'url' => $url,
        'status' => $response->getStatus(),
        'length' => strlen($body),
    ];
}

$urls = [
    'https://www.php.net',
    'https://laravel.com',
    'https://symfony.com',
];

$futures = [];
foreach ($urls as $url) {
    $futures[] = async(fetchUrl(...), $client, $url);
}

$results = \Amp\Future\await($futures);
foreach ($results as $r) {
    echo "{$r['url']}: {$r['status']} ({$r['length']} bytes)\n";
}

parallel (extension threads reales)

pecl install parallel
<?php
declare(strict_types=1);

// parallel crea hilos reales (OS threads) con memoria compartida
// Solo disponible como extensión PECL (no incluida en PHP estándar)

$task = function (array $numbers): array {
    // Esto se ejecuta en un hilo separado
    $primes = [];
    foreach ($numbers as $n) {
        if ($n < 2) continue;
        $isPrime = true;
        for ($i = 2; $i <= (int) sqrt($n); $i++) {
            if ($n % $i === 0) {
                $isPrime = false;
                break;
            }
        }
        if ($isPrime) $primes[] = $n;
    }
    return $primes;
};

$numbers = range(1, 100000);
$chunks = array_chunk($numbers, 25000);

$futures = [];
foreach ($chunks as $chunk) {
    $futures[] = \parallel\run($task, [$chunk]);
}

$allPrimes = [];
foreach ($futures as $future) {
    $allPrimes = array_merge($allPrimes, $future->value());
}

echo "Primos encontrados: " . count($allPrimes) . "\n";

Fibers (PHP 8.1+)

<?php
declare(strict_types=1);

// Fibers son la primitiva de concurrencia cooperativa de PHP 8.1
// Permiten pausar/reanudar funciones, similar a corutinas

function tarea(int $id, int $segundos): void
{
    echo "Tarea {$id}: inicio\n";
    Fiber::suspend("tarea_{$id}_suspendida");  // Pausa
    echo "Tarea {$id}: después de suspender\n";
}

$fiber1 = new Fiber(function () {
    return tarea(1, 2);
});

$fiber2 = new Fiber(function () {
    return tarea(2, 3);
});

// Ejecutar fibras de forma cooperativa
$result1 = $fiber1->start();  // "Tarea 1: inicio"
$result2 = $fiber2->start();  // "Tarea 2: inicio"

$fiber1->resume();  // "Tarea 1: después de suspender"
$fiber2->resume();  // "Tarea 2: después de suspender"

// En la práctica, amphp abstrae los Fibers con una API más amigable

¿Cuándo usar cada modelo?

EscenarioModeloEjemplo
Múltiples llamadas HTTP externasamphp / ReactPHPAPI Gateway, scraper
WebSocket server en tiempo realSwoole / RatchetChat, notificaciones
Tareas backgroundCola (Redis + worker)Enviar emails, procesar imágenes
Cálculo intensivo (CPU)parallel / pcntlProcesamiento de datos, reporting
API de alto rendimientoSwoole (reemplaza FPM)Microservicios con mucha carga
Baja carga, simplicidadPHP sin async (FPM)Web tradicional, CRUDs
Fuente: ReactPHP docs, amphp docs, Swoole docs, PHP parallel docs

Testing y calidad

PHPUnit — el framework de testing estándar

¿Qué es?

PHPUnit es el framework de testing más usado en PHP (creado por Sebastian Bergmann). Sigue el patrón xUnit y es el estándar de facto desde PHP 5. Alternativas: Pest (sintaxis más expresiva, construida sobre PHPUnit), PHPSpec (BDD), Behat (behavior-driven).

🔗 PHPUnit docs

Sintaxis básica

<?php
declare(strict_types=1);

use PHPUnit\Framework\TestCase;

class CalculadoraTest extends TestCase
{
    public function testSuma(): void
    {
        $calc = new Calculadora();
        $this->assertSame(4, $calc->suma(2, 2));
    }

    public function testDivisionPorCero(): void
    {
        $this->expectException(\DivisionByZeroError::class);
        $calc = new Calculadora();
        $calc->divide(10, 0);
    }

    /** @dataProvider proveedorSumas */
    public function testSumaProvider(int $a, int $b, int $esperado): void
    {
        $calc = new Calculadora();
        $this->assertSame($esperado, $calc->suma($a, $b));
    }

    public static function proveedorSumas(): array
    {
        return [
            [1, 2, 3],
            [-1, 1, 0],
            [0, 0, 0],
            [100, 200, 300],
        ];
    }
}

🧪 Cómo ejecutarlo

vendor/bin/phpunit tests/
vendor/bin/phpunit --filter testSuma
vendor/bin/phpunit --coverage-html coverage/

💡 Configuración (phpunit.xml)

<?xml version="1.0" encoding="UTF-8"?>
<phpunit
    bootstrap="vendor/autoload.php"
    colors="true"
    failOnRisky="true"
    failOnWarning="true"
>
    <testsuites>
        <testsuite name="Unit">
            <directory>tests/Unit</directory>
        </testsuite>
        <testsuite name="Feature">
            <directory>tests/Feature</directory>
        </testsuite>
    </testsuites>
    <source>
        <include>
            <directory>src</directory>
        </include>
    </source>
    <coverage>
        <report>
            <html outputDirectory="coverage"/>
        </report>
    </coverage>
</phpunit>

💡 Memoria y rendimiento

  • PHPUnit carga toda la suite en memoria — para proyectos grandes usa --order-by=defects o división por grupos
  • --coverage ralentiza los tests significativamente (2-5x más lentos)
  • Pest es más rápido en suites pequeñas por su sintaxis ligera

✅ Buenas prácticas

  • ✅ Un test por responsabilidad — usa @dataProvider para variaciones
  • ✅ Nombra tests como testNombreMetodo_estado_esperado (ej: testSuma_numerosPositivos_retornaSuma)
  • ✅ Usa mocks con PHPUnit createMock() para aislar dependencias
  • ⚠️ No uses sleep() en tests — usa ClockMock o similar
  • ❌ No hagas peticiones HTTP reales en tests unitarios — mockea HttpClient

🏗️ Metodología

Sigue la pirámide de testing: muchos tests unitarios (rápidos, aislados), algunos tests de integración (con BD/API), pocos tests E2E. En Laravel, usa RefreshDatabase para tests de integración. En Symfony, usa KernelTestCase.

🔗 Para saber más

Pest — testing expresivo

¿Qué es?

Pest es un wrapper sobre PHPUnit con sintaxis más fluida y expresiva. Ideal para equipos que prefieren escribir tests como funciones en lugar de clases.

<?php
use function Pest\Faker\fake;

test('suma básica', function () {
    $result = suma(2, 2);
    expect($result)->toBe(4);
});

test('excepción en división por cero', function () {
    expect(fn() => divide(10, 0))
        ->toThrow(DivisionByZeroError::class);
});

dataset('numeros', [
    [1, 2, 3],
    [-1, 1, 0],
    [0, 0, 0],
]);

test('suma con dataset', function ($a, $b, $expected) {
    expect(suma($a, $b))->toBe($expected);
})->with('numeros');

🔗 Pest PHP

Análisis estático: PHPStan y Psalm

¿Qué es?

PHPStan y Psalm son herramientas de análisis estático que detectan errores sin ejecutar el código. PHPStan tiene niveles de 0 a max (9 en PHPStan 1.x, 6 en PHPStan 2.x). Psalm usa niveles de 1 a 8 (más estricto = nivel más bajo).

# PHPStan
vendor/bin/phpstan analyse src/ --level max
vendor/bin/phpstan analyse src/ --generate-baseline  # Ignorar errores conocidos

# Psalm
vendor/bin/psalm --show-info=true
vendor/bin/psalm --alter --issues=MissingReturnType  # Auto-fix
# phpstan.neon
parameters:
    level: max
    paths:
        - src/
    excludePaths:
        - src/Migrations/

🔗 PHPStan, Psalm


Conceptos avanzados

Atributos (annotations nativas)

¿Qué es?

Los atributos (PHP 8.0) reemplazan las anotaciones PHPDoc (@Route, @ORM\Column) con metadatos estructurados nativos del lenguaje. Son clases que pueden leerse mediante Reflection.

<?php
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
class Route
{
    public function __construct(
        public string $path,
        public string $method = 'GET',
    ) {}
}

#[Route('/api/users')]
class UserController
{
    #[Route('/api/users/{id}', method: 'GET')]
    public function show(int $id): array
    {
        return ['id' => $id, 'name' => 'Ana'];
    }
}

// Leer en runtime
$ref = new ReflectionClass(UserController::class);
foreach ($ref->getAttributes(Route::class) as $attr) {
    $route = $attr->newInstance();
    echo "{$route->method} {$route->path}\n";
}

🔗 PHP 8.0 Attributes RFC

Fibers — concurrencia cooperativa

¿Qué es?

Las fibers (PHP 8.1) son corutinas a nivel de sistema. Permiten pausar y reanudar funciones en puntos específicos, similar a async/await pero sin event loop externo. amphp usa fibers como base.

<?php
$fiber = new Fiber(function (): void {
    $value = Fiber::suspend('suspendido');
    echo "Reanudado con: $value\n";
});

$result = $fiber->start();   // 'suspendido'
$fiber->resume('hola');      // Imprime: "Reanudado con: hola"

🔗 PHP 8.1 Fibers RFC

Enums nativos

¿Qué es?

Los enums (PHP 8.1) son tipos enumerados con métodos, interfaces y valores asociados. Reemplazan las constantes de clase y los paquetes de myclabs/php-enum.

<?php
enum Status: string
{
    case Draft = 'draft';
    case Published = 'published';
    case Archived = 'archived';

    public function label(): string
    {
        return match ($this) {
            self::Draft => 'Borrador',
            self::Published => 'Publicado',
            self::Archived => 'Archivado',
        };
    }
}

echo Status::Published->value;     // 'published'
echo Status::Published->label();   // 'Publicado'
echo Status::from('draft');        // Status::Draft

🔗 PHP 8.1 Enums RFC

SOAP y XML

PHP tiene soporte nativo para SOAP (SoapClient, SoapServer) y XML (SimpleXML, DOMDocument, XMLReader para archivos grandes).

<?php
// SoapClient
$client = new SoapClient('https://api.example.com/wsdl?wsdl');
$result = $client->getUser(['id' => 1]);

// SimpleXML
$xml = simplexml_load_string('<user><name>Ana</name></user>');
echo $xml->name;  // Ana

// DOMDocument
$dom = new DOMDocument();
$dom->loadHTML($html);
$items = $dom->getElementsByTagName('li');

// XMLReader para archivos grandes (>100MB)
$reader = new XMLReader();
$reader->open('large.xml');
while ($reader->read()) {
    if ($reader->nodeType === XMLReader::ELEMENT && $reader->name === 'item') {
        $item = $reader->readOuterXml();
        // procesar...
    }
}

🔗 PHP Manual — SOAP, PHP Manual — SimpleXML


Proyectos prácticos detallados

Gestor de contraseñas CLI (nivel 30)

<?php
declare(strict_types=1);

// password-manager.php
// Uso: php password-manager.php add google ana@email.com
//       php password-manager.php get google
//       php password-manager.php generate

namespace App;

class PasswordEntry
{
    public function __construct(
        public readonly string $service,
        public readonly string $username,
        public readonly string $password,
    ) {}
}

class Vault
{
    private string $path;
    private string $keyPath;

    public function __construct(?string $path = null)
    {
        $this->path = $path ?? __DIR__ . '/vault.enc';
        $this->keyPath = __DIR__ . '/vault.key';
    }

    public function add(PasswordEntry $entry): void
    {
        $store = $this->load();
        $store[$entry->service] = [
            'service' => $entry->service,
            'username' => $entry->username,
            'password' => $entry->password,
        ];
        $this->save($store);
    }

    public function get(string $service): ?PasswordEntry
    {
        $store = $this->load();
        if (!isset($store[$service])) {
            return null;
        }
        $data = $store[$service];
        return new PasswordEntry($data['service'], $data['username'], $data['password']);
    }

    private function load(): array
    {
        if (!file_exists($this->path)) {
            return [];
        }
        $encrypted = file_get_contents($this->path);
        $decrypted = openssl_decrypt(
            $encrypted,
            'aes-256-gcm',
            $this->getKey(),
            0,
            str_repeat("\0", 12),
            $tag ?? '',
        );
        return json_decode($decrypted, true) ?? [];
    }

    private function save(array $data): void
    {
        $json = json_encode($data, JSON_PRETTY_PRINT);
        $iv = random_bytes(12);
        $encrypted = openssl_encrypt($json, 'aes-256-gcm', $this->getKey(), 0, $iv, $tag);
        file_put_contents($this->path, $encrypted);
    }

    private function getKey(): string
    {
        if (!file_exists($this->keyPath)) {
            $key = random_bytes(32);
            file_put_contents($this->keyPath, $key);
        }
        return file_get_contents($this->keyPath);
    }
}

function generatePassword(int $length = 20): string
{
    $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()';
    $password = '';
    $max = strlen($chars) - 1;
    for ($i = 0; $i < $length; $i++) {
        $password .= $chars[random_int(0, $max)];
    }
    return $password;
}

// CLI handler
$command = $argv[1] ?? 'help';

match ($command) {
    'add' => function () use ($argv) {
        $service = $argv[2] ?? throw new \InvalidArgumentException('Service required');
        $username = $argv[3] ?? throw new \InvalidArgumentException('Username required');
        $password = $argv[4] ?? generatePassword();
        $vault = new Vault();
        $vault->add(new PasswordEntry($service, $username, $password));
        echo "✓ Contraseña guardada para {$service}\n";
    },
    'get' => function () use ($argv) {
        $service = $argv[2] ?? throw new \InvalidArgumentException('Service required');
        $vault = new Vault();
        $entry = $vault->get($service);
        if ($entry === null) {
            echo "✗ No se encontró {$service}\n";
            return;
        }
        echo "Servicio: {$entry->service}\n";
        echo "Usuario: {$entry->username}\n";
        echo "Contraseña: {$entry->password}\n";
    },
    'generate' => function () use ($argv) {
        $length = (int) ($argv[2] ?? 20);
        echo "🔑 " . generatePassword($length) . "\n";
    },
    default => function () {
        echo "Uso: php password-manager.php <comando> [args]\n";
        echo "  add <service> <username> [password]  - Guardar contraseña\n";
        echo "  get <service>                        - Obtener contraseña\n";
        echo "  generate [length]                    - Generar contraseña\n";
    },
};

Web scraper con async (nivel 60) — usando amphp

<?php
declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

use function Amp\async;
use function Amp\Future\await;
use Amp\Http\Client\HttpClientBuilder;
use Amp\Http\Client\Request;
use Symfony\Component\DomCrawler\Crawler;

class Article
{
    public function __construct(
        public readonly string $title,
        public readonly string $url,
        public readonly string $source,
    ) {}
}

function scrapeNews(string $url, HttpClientBuilder $client): array
{
    $request = new Request($url);
    $response = $client->request($request);
    $html = $response->getBody()->buffer();

    $crawler = new Crawler($html);
    $articles = [];

    $crawler->filter('article h2 a, .headline a, h2 a')->each(function (Crawler $node) use ($url, &$articles) {
        $title = trim($node->text());
        $href = $node->attr('href');
        if ($href && $title) {
            $fullUrl = str_starts_with($href, 'http') ? $href : rtrim($url, '/') . '/' . ltrim($href, '/');
            $articles[] = new Article($title, $fullUrl, $url);
        }
    });

    return $articles;
}

$sources = [
    'https://news.ycombinator.com',
    'https://www.php.net/',
    'https://laravel-news.com/blog',
];

$client = HttpClientBuilder::buildDefault();
$futures = [];

foreach ($sources as $source) {
    $futures[] = async(scrapeNews(...), $source, $client);
}

$results = await($futures);
$allArticles = array_merge(...$results);

foreach (array_slice($allArticles, 0, 10) as $article) {
    echo "[{$article->source}] {$article->title}" . PHP_EOL;
}

echo "Total artículos: " . count($allArticles) . PHP_EOL;

Proyecto final integrador: API REST de gestión de tareas

Construye una API REST completa con PHP puro (sin framework) primero, y luego migra a Laravel para ver la diferencia. Este proyecto cubre todos los conceptos de la guía.

Fase 1 — PHP puro con router casero

<?php
declare(strict_types=1);

// public/index.php — Entry point
require_once __DIR__ . '/../vendor/autoload.php';

use App\Router;
use App\Controllers\TaskController;
use App\Repositories\JsonTaskRepository;

$container = [
    'taskRepo' => new JsonTaskRepository(__DIR__ . '/../data/tasks.json'),
];

$router = new Router();

$router->get('/api/tasks',         [new TaskController($container['taskRepo']), 'index']);
$router->get('/api/tasks/{id}',    [new TaskController($container['taskRepo']), 'show']);
$router->post('/api/tasks',        [new TaskController($container['taskRepo']), 'store']);
$router->put('/api/tasks/{id}',    [new TaskController($container['taskRepo']), 'update']);
$router->delete('/api/tasks/{id}', [new TaskController($container['taskRepo']), 'destroy']);

$router->dispatch($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI']);

Fase 2 — Migración a Laravel

composer create-project laravel/laravel task-manager
php artisan make:model Task -m
php artisan make:controller Api/TaskController --api
php artisan make:request StoreTaskRequest
php artisan make:resource TaskResource
<?php
// app/Models/Task.php
class Task extends Model
{
    protected $fillable = ['title', 'description', 'completed', 'due_date'];

    protected function casts(): array
    {
        return [
            'completed' => 'boolean',
            'due_date' => 'date',
        ];
    }
}

// app/Http/Resources/TaskResource.php
class TaskResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'description' => $this->description,
            'completed' => $this->completed,
            'due_date' => $this->due_date?->format('Y-m-d'),
            'created_at' => $this->created_at,
        ];
    }
}

Fase 3 — Testing

<?php
// tests/Feature/TaskTest.php
class TaskTest extends TestCase
{
    use RefreshDatabase;

    public function test_puede_crear_tarea(): void
    {
        $response = $this->postJson('/api/tasks', [
            'title' => 'Aprender PHP',
            'description' => 'Completar la guía de 0 a 100',
        ]);

        $response->assertStatus(201)
                 ->assertJson(['title' => 'Aprender PHP']);
    }

    public function test_lista_tareas(): void
    {
        Task::factory()->count(3)->create();

        $response = $this->getJson('/api/tasks');
        $response->assertStatus(200)
                 ->assertJsonCount(3, 'data');
    }

    public function test_requiere_titulo(): void
    {
        $response = $this->postJson('/api/tasks', []);
        $response->assertStatus(422);
    }
}

Fase 4 — Producción

# Optimizar autoload
composer install --no-dev --optimize-autoloader

# Cachear rutas y config
php artisan config:cache
php artisan route:cache

# Servir con PHP-FPM + Nginx (no artisan serve)
# Configuración Nginx:
# server {
#     listen 80;
#     server_name api.example.com;
#     root /var/www/task-manager/public;
#     index index.php;
#     location / { try_files $uri $uri/ /index.php?$query_string; }
#     location ~ \.php$ { fastcgi_pass unix:/var/run/php/php8.3-fpm.sock; }
# }

Despliegue a producción

Build para producción

composer install --no-dev --optimize-autoloader
php artisan config:cache
php artisan route:cache

Docker multi-stage

# Stage 1: Dependencias
FROM php:8.3-cli AS build
WORKDIR /app
COPY composer.json composer.lock ./
RUN apt-get update && apt-get install -y libpq-dev zip unzip && docker-php-ext-install pdo_pgsql
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
RUN composer install --no-dev --optimize-autoloader

# Stage 2: Runtime con FPM + Nginx
FROM php:8.3-fpm
RUN apt-get update && apt-get install -y nginx libpq-dev && docker-php-ext-install pdo_pgsql
COPY --from=build /app /var/www/app
COPY . /var/www/app
COPY nginx.conf /etc/nginx/sites-enabled/default
EXPOSE 80
CMD ["sh", "-c", "php-fpm & nginx -g 'daemon off;'"]

Hosting

CI/CD (GitHub Actions)

name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
      - run: composer install --no-dev --optimize-autoloader
      - run: php artisan config:cache
      - run: php artisan route:cache

Hacks y tips de productividad en PHP

1. match vs switch — match siempre gana

<?php
// switch: tedioso, propenso a errores (olvidar break)
switch ($status) {
    case 200:
        $mensaje = 'OK';
        break;
    case 404:
        $mensaje = 'Not Found';
        break;
    default:
        $mensaje = 'Unknown';
}

// match: expresión, exhaustivo, sin break, tipos estrictos
$mensaje = match ($status) {
    200 => 'OK',
    404 => 'Not Found',
    default => 'Unknown',
};

// match con condiciones y lógica
$nivel = match (true) {
    $puntos >= 1000 => 'VIP',
    $puntos >= 500  => 'Premium',
    $puntos >= 100  => 'Regular',
    default         => 'Nuevo',
};

2. match + enum = poder absoluto

<?php
enum Status: string
{
    case Draft = 'draft';
    case Published = 'published';
    case Archived = 'archived';

    public function label(): string
    {
        return match ($this) {
            self::Draft => 'Borrador',
            self::Published => 'Publicado',
            self::Archived => 'Archivado',
        };
    }

    public function canEdit(): bool
    {
        return match ($this) {
            self::Published => false,
            default => true,
        };
    }
}

// Uso
$status = Status::Draft;
echo $status->label();     // 'Borrador'
echo $status->canEdit();   // true

3. Named arguments — adiós al orden de parámetros

<?php
// Antes: tenías que recordar el orden exacto
$response = new Response('', 302, ['Location' => '/']);

// Con named arguments: orden arbitrario, saltas defaults
$response = new Response(
    headers: ['Location' => '/'],
    status: 302,
);

// Especialmente útil en Laravel/Symfony con métodos largos
User::create(
    name: 'Ana',
    email: 'ana@email.com',
    password: bcrypt('secret'),
    email_verified_at: now(),
    role: 'admin',
);

4. Readonly properties — inmutabilidad fácil (PHP 8.1+)

<?php
// Antes: getters + propiedades private
class UsuarioOld
{
    private string $nombre;
    public function __construct(string $nombre)
    {
        $this->nombre = $nombre;
    }
    public function nombre(): string { return $this->nombre; }
}

// Con readonly + constructor promotion
class Usuario
{
    public function __construct(
        public readonly string $nombre,
        public readonly string $email,
        public readonly ?int $edad = null,
    ) {}
}

// PHP 8.3+: readonly modificable en __clone
$user = new Usuario('Ana', 'ana@email.com');
// $user->nombre = 'Luis'; // Error! Propiedad readonly

5. Array unpacking y spread (PHP 7.4+)

<?php
// Spread en arrays (como ... en JavaScript)
$part1 = [1, 2, 3];
$part2 = [4, 5, 6];
$completo = [...$part1, ...$part2];  // [1, 2, 3, 4, 5, 6]

// Unpacking de argumentos (...)
$nums = [1, 2, 3, 4, 5];
$suma = array_sum(...$nums);  // Equivalente a array_sum(1, 2, 3, 4, 5) 

// En funciones variádicas
function sumarTodos(int ...$numeros): int
{
    return array_sum($numeros);
}
echo sumarTodos(...[1, 2, 3]);  // 6

6. First-class callable syntax (PHP 8.1+)

<?php
// Antes: strings o arrays para callables
$upper = 'strtoupper';
$method = [$this, 'metodo'];

// PHP 8.1: sintaxis (...)
$upper = strtoupper(...);
$method = $this->metodo(...);

// Especialmente útil con array_map
$nombres = ['ana', 'luis', 'pedro'];
$mayusculas = array_map(strtoupper(...), $nombres);

// Con closures
$duplicar = fn(int $n) => $n * 2;
$duplicados = array_map($duplicar(...), [1, 2, 3]);

7. Los 3 comandos que todo proyecto PHP debería tener

composer check                 # Ejecuta lint + phpstan + test
vendor/bin/phpstan analyse src --level=max  # Análisis estático
vendor/bin/pint                # Formatea código (Laravel Pint)

Configúralos como scripts de Composer:

{
    "scripts": {
        "check": ["@lint", "@phpstan", "@test"],
        "lint": "vendor/bin/pint --test",
        "fix": "vendor/bin/pint",
        "phpstan": "phpstan analyse src --level=max",
        "test": "phpunit"
    }
}

8. Tip: variables de entorno con phpdotenv

<?php
// Instalar: composer require vlucas/phpdotenv

$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();

// Acceso seguro (lanza excepción si no existe)
$dbUrl = $_ENV['DATABASE_URL'];

// Con valor por defecto
$debug = $_ENV['DEBUG'] ?? false;

// Validación
$dotenv->required('DB_PASSWORD')->notEmpty();
$dotenv->required('APP_ENV')->allowedValues(['dev', 'test', 'prod']);

// .env file
/*
APP_ENV=dev
DATABASE_URL=mysql://root:secret@127.0.0.1:3306/blog
DEBUG=true
*/

9. El depurador con dd()

<?php
// Laravel: dd() = dump + die
$usuario = User::find(1);
dd($usuario);  // Muestra el objeto y detiene la ejecución

// Sin Laravel, puedes usar:
function dd(mixed ...$vars): never
{
    foreach ($vars as $var) {
        var_dump($var);
    }
    exit(1);
}

// dump() solo muestra sin detener
dump($usuario);

10. Nullsafe + null coalescing = menos ifs

<?php
// Antes: cadenas de null checks
$ciudad = null;
if ($usuario !== null) {
    if ($usuario->direccion !== null) {
        $ciudad = $usuario->direccion->ciudad;
    }
}

// PHP 8.0+ nullsafe
$ciudad = $usuario?->direccion?->ciudad;

// Con default
$ciudad ??= 'No especificada';

// Combinado: país o default
$pais = $usuario?->direccion?->pais ?? 'Desconocido';
Fuente: PHP 8.0 — Match Expression RFC, PHP 8.1 — Enums RFC, PHP 8.1 — Readonly Properties, PHP 8.1 — First-class Callable

Canales y recursos en español

YouTube

  • CodigoFacilito — Curso de PHP desde cero, Laravel, buenas prácticas. Uno de los canales más completos en español
  • MoureDev — PHP, automatización, proyectos. También streams en Twitch con PHP
  • Develoteca — PHP, Laravel, WordPress, proyectos web completos paso a paso
  • Carlos Alfaro — PHP moderno, PHP 8, Laravel, Symfony, testing
  • Fatz — PHP, Laravel, APIs, desarrollo web práctico
  • Programación Fácil — Tutoriales de PHP para principiantes, muy estructurados
  • Bluuweb — Laravel, PHP, desarrollo web full-stack con ejemplos prácticos
  • midulive — PHP y desarrollo web en general, con estilo directo y pragmático

Comunidades

  • r/PHP (Reddit) — Noticias, discusiones, proyectos, dudas (inglés, pero comunidad activa)
  • PHP Hispano (Facebook) — Grupo grande de PHP en español
  • Laravel España (Telegram) — Comunidad activa de Laravel en español
  • Symfony España (Telegram) — Para usuarios de Symfony y componentes
  • Stack Overflow en español — Etiqueta php para dudas técnicas
  • PHP Developers Spain (Discord) — Servidor Discord con canales de ayuda y proyectos
  • Laravel News (Newsletter) — Noticias semanales sobre Laravel y ecosistema PHP

Blogs y newsletters

  • Laravel News (laravel-news.com) — Blog sobre Laravel y ecosistema PHP
  • PHP.Watch (php.watch) — Novedades de cada versión de PHP, cambios y retrocompatibilidad
  • Laravel Daily (laraveldaily.com) — Tips, tutoriales y trucos de Laravel
  • PHP The Right Way (phptherightway.com) — Guía de buenas prácticas en PHP (también en español)
  • Styde (styde.net) — Cursos y tutoriales de PHP, Laravel y Symfony en español
  • PHP Developer Blog (Medium) — Artículos técnicos sobre PHP moderno

Repositorios de código

Fuente: búsqueda en YouTube y Reddit, listas curadas de Awesome PHP

Referencias y documentación oficial

Fuentes de esta guía

Esta guía se ha elaborado a partir de las siguientes fuentes consultadas durante su desarrollo:


⚠️ Errores comunes

Estos son los fallos que más se repiten al empezar con PHP. La mayoría son de sintaxis, variables o conexiones de red.

ErrorCausaSolución
syntax error, unexpected tokenFalta un ;, un $ o una llave } en la línea anteriorRevisa el ; al final de cada sentencia y que la variable lleve $
Call to undefined function X()Llamas a una función que no existe o no incluyes su ficheroComprueba el nombre y que hayas hecho require/include del archivo
Fatal error: Uncaught Error: Call to a member function on nullAccedes a un método sobre un objeto que es nullVerifica que la consulta/función devolvió un resultado antes de llamar al método
Warning: Undefined array key "x" (PHP 8)Accedes a una clave de array que no existeUsa isset() o el operador ?? (null coalescing) antes de acceder
Connection refused con mysqli/PDOEl servidor de base de datos no está activo o el puerto es incorrectoComprueba que MySQL/MariaDB esté corriendo y que host, puerto, usuario y contraseña son correctos
curl: (56) Failure when receiving data from the peer al descargar ComposerProblema de red, proxy o versión de TLSRepite el curl, revisa proxy y usa HTTPS; prueba con composer self-update
Fatal error: Uncaught PDOExceptionCredenciales de BD erróneas o driver ausenteRevisa DSN, usuario y contraseña, y habilita la extensión pdo_mysql en php.ini

🔗 Guías relacionadas


🏆 Retos Relacionados

Pon a prueba lo aprendido con estos desafíos:

COMPARTIR:
ETIQUETADO EN:
COMENTARIOS:

📋 Contenido