Módulo 13: Namespaces y Composer
Objetivos del módulo
- Entender el problema de la colisión de nombres y cómo lo resuelven los namespaces
- Declarar namespaces e importar con
use: alias,use function,use consty agrupado - Organizar el código en carpetas que reflejen los namespaces
- Escribir un autoloader manual con
spl_autoload_register() - Instalar y usar Composer:
composer.json,require,install,updateycomposer.lock - Comprender el versionado semántico y las restricciones
^y~ - Configurar el autoload PSR-4 de Composer
- Buscar paquetes en Packagist y usar librerías populares
- Conocer los estándares PSR más importantes y el estilo PER Coding Style
- Dar los primeros pasos con PHP-CS-Fixer y PHPStan
- Montar un proyecto profesional completo con
src/ytests/
1. El problema: colisión de nombres
Imagina que tu tienda online tiene una clase Usuario. Instalas una librería de pagos que también tiene una clase Usuario, y otra de correo electrónico que tiene una clase Logger, igual que la tuya:
<?php
require 'mi-app/Usuario.php';
require 'libreria-pagos/Usuario.php';
// Fatal error: Cannot declare class Usuario, because the name is already in use
Antes de los namespaces (PHP < 5.3), la solución era usar prefijos larguísimos: Zend_Db_Adapter_Pdo_Mysql, PHPUnit_Framework_TestCase… Incómodo e ilegible.
📘 Concepto: Un namespace (espacio de nombres) es como un apellido para tus clases, funciones y constantes. En una clase puede haber dos alumnos llamados “Lucía”, pero “Lucía García” y “Lucía Martín” no se confunden. Del mismo modo,
App\Modelo\UsuarioyPagos\Usuariopueden convivir sin problemas.
Otra analogía útil: los namespaces funcionan como las carpetas de tu disco. Puedes tener dos ficheros notas.txt siempre que estén en carpetas distintas.
2. Declarar un namespace
<?php
// src/Modelo/Usuario.php
declare(strict_types=1);
namespace App\Modelo;
final class Usuario
{
public function __construct(public readonly string $nombre) {}
}
| Regla | Detalle |
|---|---|
| Posición | namespace debe ser la primera instrucción del fichero (solo puede ir antes declare(strict_types=1);) |
| Separador | Barra invertida \: App\Modelo\Usuario |
| Convención | PascalCase, empezando por el nombre del proveedor o del proyecto: App\, Acme\Tienda\ |
| Qué afecta | Clases, interfaces, traits, enums, funciones y constantes (const) |
| Uno por fichero | Técnicamente se pueden poner varios, pero la convención es un namespace y una clase por fichero |
Nombre completamente cualificado
El nombre completo (FQCN, Fully Qualified Class Name) de la clase anterior es App\Modelo\Usuario. Con una barra inicial (\App\Modelo\Usuario) indicas que es absoluto, desde la raíz:
<?php
declare(strict_types=1);
namespace App\Controlador;
$u1 = new \App\Modelo\Usuario('Ana'); // Absoluto: siempre funciona
$u2 = new Modelo\Usuario('Luis'); // ❌ Relativo: buscaría App\Controlador\Modelo\Usuario
echo \App\Modelo\Usuario::class . PHP_EOL; // App\Modelo\Usuario
💡 Consejo: La constante
::classdevuelve el nombre completo de una clase como cadena. Úsala siempre en lugar de escribir el nombre a mano:Usuario::classen vez de'App\Modelo\Usuario'. El IDE la entiende y la actualiza si renombras la clase.
Clases globales dentro de un namespace
Dentro de un namespace, todas las clases se buscan en ese namespace. Las clases nativas de PHP (DateTimeImmutable, PDO, Exception…) viven en el namespace global:
<?php
declare(strict_types=1);
namespace App\Servicio;
$fecha = new DateTimeImmutable(); // ❌ Error: Class "App\Servicio\DateTimeImmutable" not found
$fecha = new \DateTimeImmutable(); // ✅ Con barra inicial
// Las FUNCIONES y CONSTANTES nativas sí tienen "plan B":
// si no existe App\Servicio\strlen, PHP usa la global strlen
echo strlen('hola'); // ✅ Funciona sin barra
⚠️ Importante: Esta diferencia confunde mucho al principio: si una función o constante no existe en el namespace actual, PHP la busca en el global; con las clases no hay fallback. Así que, en ficheros con namespace, las clases nativas necesitan
\delante o unuse.
3. Importar con use
Escribir nombres completos en todas partes sería pesado. use importa un nombre para usarlo de forma corta en el fichero:
<?php
declare(strict_types=1);
namespace App\Controlador;
use App\Modelo\Usuario;
use App\Modelo\Producto;
use App\Servicio\Notificador;
use DateTimeImmutable; // Importar una clase global
use InvalidArgumentException;
final class RegistroController
{
public function __construct(private Notificador $notificador) {}
public function registrar(string $nombre): Usuario
{
if ($nombre === '') {
throw new InvalidArgumentException('Nombre vacío');
}
$usuario = new Usuario($nombre);
$this->notificador->bienvenida($usuario, new DateTimeImmutable());
return $usuario;
}
}
📘 Concepto:
useno carga ningún fichero ni ejecuta nada. Solo crea un alias para el resto del fichero: “cuando escribaUsuario, me refiero aApp\Modelo\Usuario”. La carga del fichero la hace el autoloader (sección 5).
Alias con as
Si importas dos clases con el mismo nombre corto, renombra una de ellas:
<?php
declare(strict_types=1);
namespace App\Servicio;
use App\Modelo\Usuario;
use Pagos\Usuario as UsuarioPagos;
use Psr\Log\LoggerInterface as Logger;
function sincronizar(Usuario $local, UsuarioPagos $remoto, Logger $log): void
{
$log->info("Sincronizando {$local->nombre}");
}
use function y use const
<?php
// src/Util/texto.php
declare(strict_types=1);
namespace App\Util;
const SEPARADOR = '-';
function slugify(string $texto): string
{
return $texto
|> trim(...)
|> mb_strtolower(...)
|> (fn(string $s) => preg_replace('/[^a-z0-9]+/u', SEPARADOR, $s))
|> (fn(string $s) => trim($s, SEPARADOR));
}
<?php
declare(strict_types=1);
namespace App\Controlador;
use function App\Util\slugify;
use const App\Util\SEPARADOR;
echo slugify('Hola Mundo PHP') . PHP_EOL; // hola-mundo-php
echo SEPARADOR . PHP_EOL; // -
Importación agrupada
<?php
declare(strict_types=1);
namespace App\Controlador;
use App\Modelo\{Usuario, Producto, Pedido};
use App\Servicio\{Notificador, CalculadoraPrecios as Calculadora};
use function App\Util\{slugify, formatearPrecio};
| Forma | Ejemplo |
|---|---|
| Clase | use App\Modelo\Usuario; |
| Clase con alias | use Pagos\Usuario as UsuarioPagos; |
| Clase global | use DateTimeImmutable; |
| Función | use function App\Util\slugify; |
| Constante | use const App\Util\SEPARADOR; |
| Agrupado | use App\Modelo\{Usuario, Producto}; |
| Namespace parcial | use App\Modelo; → new Modelo\Usuario() |
💡 Consejo: Ordena los
usealfabéticamente y elimina los que no uses. Los IDE (y PHP-CS-Fixer, sección 11) lo hacen automáticamente.
4. Estructura de carpetas
La convención universal es que la estructura de carpetas refleje los namespaces: el namespace App\Modelo está en la carpeta src/Modelo/, y la clase App\Modelo\Usuario en el fichero src/Modelo/Usuario.php.
mi-proyecto/
├── public/
│ └── index.php ← Único punto de entrada web
├── src/ ← Namespace raíz App\
│ ├── Controlador/
│ │ └── ProductoController.php → App\Controlador\ProductoController
│ ├── Modelo/
│ │ ├── Producto.php → App\Modelo\Producto
│ │ └── Usuario.php → App\Modelo\Usuario
│ └── Servicio/
│ └── CalculadoraPrecios.php → App\Servicio\CalculadoraPrecios
└── tests/ ← Namespace raíz App\Tests\
└── Modelo/
└── ProductoTest.php → App\Tests\Modelo\ProductoTest
| Elemento | Convención |
|---|---|
| Namespace | PascalCase, coincide con las carpetas |
| Nombre del fichero | Igual que la clase, respetando mayúsculas: Producto.php |
| Una clase por fichero | Siempre |
| Código web público | Solo public/ es accesible desde el navegador |
⚠️ Importante: Respeta exactamente las mayúsculas en nombres de carpetas y ficheros. En Windows y macOS el sistema de ficheros no distingue mayúsculas, así que
src/modelo/producto.php“funciona” en tu ordenador… y falla en el servidor Linux de producción.
5. Autoload manual con spl_autoload_register()
Con un fichero por clase, un proyecto mediano tiene cientos de ficheros. Escribir un require_once para cada uno sería insostenible.
📘 Concepto: Un autoloader es una función que PHP llama automáticamente cuando usas una clase que todavía no está cargada, pasándole su nombre completo. La función traduce ese nombre a una ruta de fichero y lo incluye. Así, cada clase se carga solo cuando se necesita.
<?php
// autoload.php
declare(strict_types=1);
spl_autoload_register(function (string $clase): void {
$prefijo = 'App\\'; // Namespace raíz del proyecto
$carpetaBase = __DIR__ . '/src/'; // Carpeta donde vive
// ¿La clase pertenece a nuestro namespace?
if (!str_starts_with($clase, $prefijo)) {
return; // No es nuestra: que lo intente otro autoloader
}
// App\Modelo\Producto → Modelo\Producto → Modelo/Producto.php
$relativa = substr($clase, strlen($prefijo));
$fichero = $carpetaBase . str_replace('\\', '/', $relativa) . '.php';
if (is_file($fichero)) {
require $fichero;
}
});
<?php
// public/index.php
declare(strict_types=1);
require __DIR__ . '/../autoload.php';
use App\Modelo\Producto;
use App\Servicio\CalculadoraPrecios;
$p = new Producto('Café', 12.5); // PHP llama al autoloader con "App\Modelo\Producto"
echo new CalculadoraPrecios()->conIva($p); // Y aquí con "App\Servicio\CalculadoraPrecios"
flowchart LR
A["new Producto(...)"] --> B{"¿Clase cargada?"}
B -- Sí --> F[Crear objeto]
B -- No --> C["Autoloader recibe<br/>'App\Modelo\Producto'"]
C --> D["Traduce a<br/>src/Modelo/Producto.php"]
D --> E[require del fichero]
E --> F
Este autoloader sigue el estándar PSR-4, que define precisamente esta correspondencia entre namespace y ruta. En la práctica no lo escribirás a mano: Composer lo genera por ti.
6. Composer
📘 Concepto: Composer es el gestor de dependencias de PHP. Lee un fichero
composer.jsondonde declaras qué librerías necesita tu proyecto (y en qué versiones), las descarga desde Packagist (el repositorio público de paquetes PHP), resuelve las dependencias de esas dependencias y genera un autoloader para todo. Es el equivalente anpmen JavaScript opipen Python.
Sin Composer tendrías que descargar cada librería a mano, comprobar qué versiones son compatibles entre sí, actualizarlas una a una e incluir sus ficheros. Con Composer, una sola orden lo resuelve.
Instalación
Windows: descarga y ejecuta el instalador Composer-Setup.exe desde https://getcomposer.org/download/. Detecta tu php.exe y añade composer al PATH.
Linux y macOS: sigue las instrucciones de la misma página (incluyen la verificación del hash del instalador). En esencia:
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php composer-setup.php
php -r "unlink('composer-setup.php');"
sudo mv composer.phar /usr/local/bin/composer
En macOS también puedes usar Homebrew (brew install composer). Comprueba la instalación:
composer --version
# Composer version 2.x.x ...
composer self-update # Actualizar Composer
Crear un proyecto: composer init
mkdir tienda && cd tienda
composer init
El asistente te pregunta el nombre del paquete (proveedor/proyecto, por ejemplo fran/tienda), descripción, autor, tipo, licencia, dependencias y el namespace para el autoload PSR-4. Al terminar crea el composer.json.
Añadir dependencias: composer require
# Dependencia de producción
composer require monolog/monolog
# Dependencia solo de desarrollo (pruebas, análisis, depuración)
composer require --dev phpunit/phpunit phpstan/phpstan symfony/var-dumper
# Con una restricción de versión concreta
composer require nesbot/carbon:^3.0
composer require hace tres cosas: añade el paquete a composer.json, lo descarga en vendor/ y actualiza composer.lock.
El fichero composer.json
{
"name": "fran/tienda",
"description": "Tienda online de ejemplo del Curso de PHP 8.5",
"type": "project",
"license": "MIT",
"authors": [
{ "name": "Fran", "email": "fran@example.com" }
],
"require": {
"php": ">=8.5",
"ext-pdo": "*",
"monolog/monolog": "^3.9",
"nesbot/carbon": "^3.8",
"vlucas/phpdotenv": "^5.6"
},
"require-dev": {
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^12.0",
"symfony/var-dumper": "^8.0"
},
"autoload": {
"psr-4": {
"App\\": "src/"
},
"files": [
"src/Util/funciones.php"
]
},
"autoload-dev": {
"psr-4": {
"App\\Tests\\": "tests/"
}
},
"scripts": {
"test": "phpunit",
"analizar": "phpstan analyse"
},
"config": {
"sort-packages": true
}
}
| Clave | Para qué sirve |
|---|---|
name | Identificador proveedor/paquete (minúsculas) |
type | project para aplicaciones, library para librerías reutilizables |
require | Dependencias necesarias en producción. Incluye la versión de PHP (php) y extensiones (ext-pdo, ext-mbstring) |
require-dev | Dependencias solo para desarrollo: no se instalan con --no-dev |
autoload.psr-4 | Correspondencia namespace → carpeta |
autoload.files | Ficheros que se cargan siempre (para funciones, que no se autocargan) |
autoload-dev | Autoload solo en desarrollo (tests) |
scripts | Comandos personalizados: composer test |
config | Opciones de Composer (sort-packages ordena las dependencias alfabéticamente) |
⚠️ Importante: En JSON la barra invertida se escapa: el namespace
App\se escribe"App\\". Y la clave del namespace debe terminar en\\.
vendor/ y el autoloader
Composer descarga todas las dependencias en la carpeta vendor/ y genera vendor/autoload.php. Solo tienes que incluir ese fichero una vez, en el punto de entrada, y todas las clases (las tuyas y las de las librerías) se cargarán automáticamente:
<?php
// public/index.php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use App\Modelo\Producto;
use Monolog\Logger;
$log = new Logger('tienda');
$producto = new Producto('Café', 12.5);
install frente a update y el papel de composer.lock
| Comando | Qué hace | Cuándo usarlo |
|---|---|---|
composer install | Instala exactamente las versiones de composer.lock. Si no existe, resuelve y lo crea | Al clonar un proyecto, en producción, en CI |
composer update | Busca las versiones más nuevas que permite composer.json y reescribe composer.lock | Cuando quieres actualizar dependencias a propósito |
composer update monolog/monolog | Actualiza solo ese paquete | Actualización controlada |
composer require x/y | Añade un paquete nuevo | Nueva dependencia |
composer remove x/y | Elimina un paquete | Dependencia que ya no usas |
📘 Concepto:
composer.jsondice qué versiones aceptas (“cualquier Monolog 3.x”).composer.lockregistra qué versión exacta se instaló (Monolog 3.9.0, con su hash), junto con todas las dependencias indirectas. Es la “foto” del estado exacto devendor/.
flowchart LR
J[composer.json<br/>restricciones: ^3.9] -->|composer update| L[composer.lock<br/>versiones exactas: 3.9.0]
L -->|composer install| V[vendor/<br/>código descargado]
J -.->|composer install sin lock| L
⚠️ Importante: En una aplicación,
composer.lockse sube a Git. Así todo el equipo, el servidor de integración continua y producción ejecutan exactamente las mismas versiones. Sin él,composer installpodría instalar una versión distinta en cada máquina y aparecerían los temidos “en mi ordenador funciona”. (En una librería reutilizable, el lock no se usa al instalarla como dependencia de otro proyecto.)
vendor/ y .gitignore
La carpeta vendor/ no se sube a Git: se puede regenerar en cualquier momento con composer install a partir del composer.lock.
# .gitignore
/vendor/
/var/
.env
.phpunit.cache/
.php-cs-fixer.cache
Otros comandos útiles
| Comando | Para qué |
|---|---|
composer install --no-dev --optimize-autoloader | Instalación para producción (sin dependencias de desarrollo, autoload optimizado) |
composer dump-autoload | Regenera el autoloader (tras cambiar autoload en composer.json) |
composer dump-autoload -o | Autoloader optimizado (classmap) para producción |
composer outdated | Lista dependencias con versiones más nuevas disponibles |
composer show / composer show -t | Paquetes instalados / en forma de árbol |
composer why vendor/paquete | Qué paquete exige esa dependencia |
composer audit | Busca vulnerabilidades de seguridad conocidas en tus dependencias |
composer validate | Comprueba que composer.json es correcto |
composer create-project symfony/skeleton mi-app | Crea un proyecto a partir de una plantilla |
composer run-script test o composer test | Ejecuta un script |
Scripts
{
"scripts": {
"test": "phpunit --colors=always",
"analizar": "phpstan analyse --memory-limit=512M",
"estilo": "php-cs-fixer fix --dry-run --diff",
"estilo-arreglar": "php-cs-fixer fix",
"servidor": "php -S localhost:8000 -t public",
"comprobar": ["@estilo", "@analizar", "@test"]
}
}
composer test # Ejecuta PHPUnit
composer servidor # Arranca el servidor de desarrollo
composer comprobar # Estilo + análisis + pruebas, en ese orden
💡 Consejo: Dentro de los scripts, Composer añade
vendor/binal PATH, por eso puedes escribirphpuniten lugar devendor/bin/phpunit. Con@nombrellamas a otro script.
7. Versionado semántico y restricciones
📘 Concepto: El versionado semántico (SemVer) numera las versiones como
MAYOR.MENOR.PARCHE(por ejemplo3.9.2) con un significado preciso:
- MAYOR (3): cambios incompatibles con versiones anteriores. Actualizar puede romper tu código.
- MENOR (9): funcionalidades nuevas compatibles.
- PARCHE (2): corrección de errores compatible.
Restricciones de versión
| Restricción | Significa | Rango |
|---|---|---|
3.9.2 | Exactamente esa versión | =3.9.2 |
^3.9 | Compatible con 3.9: no cambia la versión mayor | >=3.9.0 <4.0.0 |
^3.9.2 | Igual, desde el parche 2 | >=3.9.2 <4.0.0 |
^0.4 | En versiones 0.x, la menor se considera “mayor” | >=0.4.0 <0.5.0 |
~3.9 | Desde 3.9, puede subir la última cifra indicada… salvo la primera | >=3.9.0 <4.0.0 |
~3.9.2 | Solo parches | >=3.9.2 <3.10.0 |
>=8.5 | Esa versión o superior | >=8.5.0 |
3.* | Cualquier 3.x | >=3.0.0 <4.0.0 |
^2.0 \|\| ^3.0 | Una u otra serie | Dos rangos |
💡 Consejo: Usa
^en casi todos los casos: es lo que Composer añade por defecto concomposer require. Recibirás mejoras y correcciones, pero nunca una versión mayor que pueda romper tu código. Para actualizar a una versión mayor nueva, cambia la restricción a propósito y revisa la guía de migración del paquete.
8. Autoload PSR-4 con Composer
Con la sección autoload de composer.json, Composer genera un autoloader PSR-4 equivalente al que escribiste en la sección 5, pero más robusto y optimizable:
{
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"App\\Tests\\": "tests/"
}
}
}
| Clase | Fichero que busca Composer |
|---|---|
App\Kernel | src/Kernel.php |
App\Modelo\Producto | src/Modelo/Producto.php |
App\Servicio\Pago\Stripe | src/Servicio/Pago/Stripe.php |
App\Tests\Modelo\ProductoTest | tests/Modelo/ProductoTest.php |
⚠️ Importante: Cada vez que modifiques la sección
autoloaddecomposer.json(por ejemplo, para añadir un namespace o un fichero de funciones), ejecutacomposer dump-autoload. Para clases nuevas dentro de un namespace PSR-4 ya registrado no hace falta: el autoloader las encuentra solo.
Puedes registrar varios namespaces raíz, o un namespace en varias carpetas:
{
"autoload": {
"psr-4": {
"App\\": "src/",
"Acme\\Pagos\\": "modulos/pagos/src/",
"Legacy\\": ["lib/", "antiguo/"]
},
"files": ["src/Util/funciones.php"]
}
}
💡 Consejo: Las funciones no se pueden autocargar (el autoloader solo funciona con clases, interfaces, traits y enums). Por eso existe
autoload.files. Úsalo con moderación: si tienes muchas funciones de utilidad, quizá deberían ser métodos estáticos de una clase o servicios.
9. Packagist y paquetes útiles
Packagist (https://packagist.org) es el repositorio público donde se publican los paquetes de Composer. Antes de instalar uno, revisa: número de descargas, fecha de la última versión, versiones de PHP soportadas, licencia, issues abiertas y si tiene vulnerabilidades conocidas.
| Paquete | Para qué sirve |
|---|---|
vlucas/phpdotenv | Cargar variables de entorno desde un fichero .env |
monolog/monolog | Logs profesionales (implementa PSR-3) |
nesbot/carbon | Fechas cómodas sobre DateTime |
symfony/var-dumper | dump() y dd(): depuración legible |
guzzlehttp/guzzle | Cliente HTTP para consumir APIs |
phpunit/phpunit | Pruebas unitarias (módulo 18) |
phpstan/phpstan | Análisis estático |
friendsofphp/php-cs-fixer | Corrección automática de estilo |
vlucas/phpdotenv: configuración con .env
composer require vlucas/phpdotenv
# .env (NO se sube a Git: contiene secretos)
APP_ENV=desarrollo
APP_DEBUG=true
DB_HOST=127.0.0.1
DB_NOMBRE=tienda
DB_USUARIO=tienda_app
DB_PASSWORD=cambia-esto
<?php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use Dotenv\Dotenv;
$dotenv = Dotenv::createImmutable(dirname(__DIR__)); // Carpeta donde está .env
$dotenv->load();
$dotenv->required(['DB_HOST', 'DB_NOMBRE', 'DB_USUARIO', 'DB_PASSWORD']); // Falla si falta alguna
$host = $_ENV['DB_HOST'];
$debug = filter_var($_ENV['APP_DEBUG'] ?? false, FILTER_VALIDATE_BOOL);
💡 Consejo: Sube a Git un fichero
.env.examplecon las mismas claves pero valores de ejemplo, para que cualquiera que clone el proyecto sepa qué variables necesita.
monolog/monolog: logs profesionales
composer require monolog/monolog
<?php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use Monolog\Handler\StreamHandler;
use Monolog\Level;
use Monolog\Logger;
$log = new Logger('tienda');
$log->pushHandler(new StreamHandler(__DIR__ . '/../var/log/app.log', Level::Debug));
$log->pushHandler(new StreamHandler('php://stderr', Level::Error)); // Errores también a la consola
$log->info('Pedido creado', ['pedido_id' => 1234, 'total' => 59.90]);
$log->warning('Stock bajo', ['sku' => 'CAF-01', 'quedan' => 2]);
$log->error('Fallo en el pago', ['pasarela' => 'stripe']);
[2026-10-03T10:15:02.123456+02:00] tienda.INFO: Pedido creado {"pedido_id":1234,"total":59.9} []
[2026-10-03T10:15:02.124001+02:00] tienda.WARNING: Stock bajo {"sku":"CAF-01","quedan":2} []
nesbot/carbon: fechas legibles
composer require nesbot/carbon
<?php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use Carbon\CarbonImmutable;
$ahora = CarbonImmutable::now('Europe/Madrid')->locale('es');
$entrega = $ahora->addWeekdays(3); // 3 días laborables
echo $entrega->isoFormat('dddd D [de] MMMM') . PHP_EOL; // p. ej.: miércoles 7 de octubre
echo $ahora->subDays(10)->diffForHumans() . PHP_EOL; // hace 1 semana
var_dump($ahora->isWeekend());
symfony/var-dumper: depuración
composer require --dev symfony/var-dumper
<?php
$pedido = ['id' => 7, 'lineas' => [['sku' => 'A1', 'cantidad' => 2]], 'cliente' => new stdClass()];
dump($pedido); // Muestra la estructura con colores y desplegables (en web y en CLI)
dd($pedido); // "dump and die": muestra y detiene el script
guzzlehttp/guzzle: consumir APIs
composer require guzzlehttp/guzzle
<?php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
$cliente = new Client([
'base_uri' => 'https://api.github.com/',
'timeout' => 5.0,
'headers' => ['User-Agent' => 'curso-php-8.5', 'Accept' => 'application/vnd.github+json'],
]);
try {
$respuesta = $cliente->get('repos/php/php-src');
$repo = json_decode((string) $respuesta->getBody(), true, flags: JSON_THROW_ON_ERROR);
echo $respuesta->getStatusCode() . PHP_EOL; // 200
echo $repo['full_name'] . ' ⭐ ' . $repo['stargazers_count'] . PHP_EOL;
} catch (GuzzleException $e) {
echo 'Error al llamar a la API: ' . $e->getMessage() . PHP_EOL;
}
Lo usarás a fondo en el módulo 17.
10. Estándares PSR
📘 Concepto: Las PSR (PHP Standards Recommendations) son estándares publicados por el PHP-FIG (Framework Interoperability Group), un grupo formado por representantes de los principales proyectos PHP. Su objetivo es que librerías y frameworks distintos puedan entenderse entre sí: si tu código usa la interfaz de logs PSR-3, funciona con Monolog o con cualquier otro logger compatible.
| Estándar | Tema | Qué define |
|---|---|---|
| PSR-1 | Estilo básico | <?php o <?=, UTF-8 sin BOM, clases en PascalCase, métodos en camelCase, constantes en MAYUSCULAS |
| PSR-4 | Autoload | Correspondencia namespace → ruta de fichero |
| PSR-12 | Estilo de código ampliado | Sangría de 4 espacios, llaves, espacios, use… (sustituido en la práctica por PER-CS) |
| PER Coding Style | Estilo moderno | Evolución de PSR-12 que cubre match, enums, atributos, hooks… La versión actual es la 3.0 |
| PSR-3 | Logger | Psr\Log\LoggerInterface con debug(), info(), warning(), error()… |
| PSR-7 | Mensajes HTTP | Interfaces inmutables para peticiones y respuestas (withHeader(), withStatus()…) |
| PSR-11 | Contenedor | ContainerInterface con get() y has() (inyección de dependencias) |
| PSR-15 | Middleware | Manejadores de peticiones y middlewares HTTP |
| PSR-17 / PSR-18 | Fábricas HTTP / Cliente HTTP | Crear mensajes PSR-7 y enviarlos |
| PSR-20 | Reloj | ClockInterface::now() para poder “congelar” el tiempo en pruebas |
PSR-3 en la práctica: depender de la interfaz
<?php
declare(strict_types=1);
namespace App\Servicio;
use Psr\Log\LoggerInterface; // La interfaz, no Monolog
final class ProcesadorPedidos
{
public function __construct(private LoggerInterface $logger) {}
public function procesar(int $pedidoId): void
{
$this->logger->info('Procesando pedido {id}', ['id' => $pedidoId]);
// ...
}
}
// Se le inyecta Monolog, pero mañana podría ser cualquier otro logger PSR-3
$procesador = new ProcesadorPedidos(new \Monolog\Logger('pedidos'));
Es la inversión de dependencias del módulo 12 aplicada a todo el ecosistema.
PSR-7: mensajes inmutables
Las peticiones y respuestas PSR-7 son objetos inmutables con métodos with*(), exactamente el patrón que viste en el módulo 11 con clone with:
<?php
// Con cualquier implementación PSR-7 (por ejemplo, guzzlehttp/psr7)
$respuesta = $fabrica->createResponse(200)
->withHeader('Content-Type', 'application/json')
->withHeader('Cache-Control', 'no-store'); // Cada with* devuelve un objeto nuevo
Reglas de estilo esenciales (PSR-1 / PER-CS)
<?php
declare(strict_types=1);
namespace App\Servicio;
use App\Modelo\Producto;
use InvalidArgumentException;
final class CalculadoraPrecios
{
public const float IVA_GENERAL = 21.0;
public function __construct(
private readonly float $descuento = 0.0,
) {}
public function precioFinal(Producto $producto, int $cantidad = 1): float
{
if ($cantidad < 1) {
throw new InvalidArgumentException('Cantidad no válida');
}
$base = $producto->precio * $cantidad * (1 - $this->descuento / 100);
return round($base * (1 + self::IVA_GENERAL / 100), 2);
}
}
- 4 espacios de sangría, sin tabuladores.
- Llave de apertura de clases y métodos en una línea nueva; de estructuras de control (
if,foreach) en la misma línea. - Una línea en blanco tras
namespacey tras el bloque deuse. - Visibilidad siempre declarada; en PER-CS 3.0, las constantes de clase con visibilidad y tipo.
- Ficheros que solo contienen PHP sin etiqueta de cierre
?>.
💡 Consejo: No memorices las reglas: deja que PHP-CS-Fixer las aplique automáticamente (siguiente sección) y configura tu editor para que formatee al guardar.
11. Herramientas de calidad
PHP-CS-Fixer: estilo automático
Analiza tu código y lo reformatea según un conjunto de reglas.
composer require --dev friendsofphp/php-cs-fixer
<?php
// .php-cs-fixer.dist.php (en la raíz del proyecto)
$finder = (new PhpCsFixer\Finder())
->in([__DIR__ . '/src', __DIR__ . '/tests']);
return (new PhpCsFixer\Config())
->setRiskyAllowed(true)
->setRules([
'@PER-CS' => true, // Última versión de PER Coding Style
'declare_strict_types' => true, // Añade declare(strict_types=1) si falta
'ordered_imports' => true, // Ordena los use alfabéticamente
'no_unused_imports' => true, // Elimina los use sin usar
])
->setFinder($finder);
vendor/bin/php-cs-fixer fix --dry-run --diff # Muestra qué cambiaría, sin tocar nada
vendor/bin/php-cs-fixer fix # Aplica los cambios
💡 Consejo: PHP-CS-Fixer avisa si lo ejecutas con una versión de PHP más reciente que la que soporta oficialmente. Si te ocurre con PHP 8.5, actualiza la herramienta (
composer update friendsofphp/php-cs-fixer) antes de buscar otras soluciones.
PHPStan: análisis estático
📘 Concepto: El análisis estático examina el código sin ejecutarlo y detecta errores: llamadas a métodos que no existen, tipos incorrectos, variables sin definir, valores que pueden ser
null… Es como un corrector ortográfico para la lógica.
composer require --dev phpstan/phpstan
# phpstan.neon (formato NEON, muy similar a YAML)
parameters:
level: 6
paths:
- src
- tests
vendor/bin/phpstan analyse
------ -------------------------------------------------------------------
Line src/Servicio/CalculadoraPrecios.php
------ -------------------------------------------------------------------
24 Method App\Servicio\CalculadoraPrecios::precioFinal() should return
float but returns float|null.
31 Call to an undefined method App\Modelo\Producto::precioConIba().
------ -------------------------------------------------------------------
[ERROR] Found 2 errors
PHPStan tiene niveles de exigencia, del 0 (comprobaciones básicas) al máximo (desde PHPStan 2, el nivel 10). Empieza por un nivel bajo en proyectos existentes y súbelo poco a poco; en proyectos nuevos, empieza al menos en el 6. Aquí es donde los tipos de PHPDoc (list<string>, array<string, int>) que viste en el módulo 07 cobran todo su valor.
| Herramienta | Qué revisa | ¿Cambia el código? |
|---|---|---|
| PHP-CS-Fixer | Formato y estilo | Sí, lo reformatea |
| PHPStan | Errores de tipos y lógica | No, solo informa |
| PHPUnit (módulo 18) | Que el comportamiento sea el esperado | No, ejecuta pruebas |
12. Proyecto ejemplo completo
Vamos a montar desde cero un pequeño proyecto profesional: una calculadora de pedidos con su estructura, autoload, configuración, logs, una prueba y las herramientas de calidad.
Estructura
tienda/
├── bin/
│ └── pedido.php ← Script de consola
├── public/
│ └── index.php ← Punto de entrada web
├── src/
│ ├── Modelo/
│ │ ├── Producto.php
│ │ ├── LineaPedido.php
│ │ └── Pedido.php
│ ├── Servicio/
│ │ └── CalculadoraPedido.php
│ └── Util/
│ └── funciones.php
├── tests/
│ └── Servicio/
│ └── CalculadoraPedidoTest.php
├── var/
│ └── log/ ← Ignorada en Git
├── vendor/ ← Ignorada en Git
├── .env ← Ignorado en Git
├── .env.example
├── .gitignore
├── .php-cs-fixer.dist.php
├── composer.json
├── composer.lock
└── phpstan.neon
Paso 1: crear el proyecto e instalar dependencias
mkdir tienda && cd tienda
composer init --name=fran/tienda --type=project --autoload=src/ --no-interaction
composer require vlucas/phpdotenv monolog/monolog
composer require --dev phpunit/phpunit phpstan/phpstan friendsofphp/php-cs-fixer symfony/var-dumper
mkdir -p src/Modelo src/Servicio src/Util tests/Servicio public bin var/log
composer init --autoload=src/ configura el namespace PSR-4 a partir del nombre del paquete (Fran\Tienda\). Para usar App\, edita composer.json como se muestra:
{
"name": "fran/tienda",
"type": "project",
"require": {
"php": ">=8.5",
"monolog/monolog": "^3.9",
"vlucas/phpdotenv": "^5.6"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.0",
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^12.0",
"symfony/var-dumper": "^8.0"
},
"autoload": {
"psr-4": { "App\\": "src/" },
"files": ["src/Util/funciones.php"]
},
"autoload-dev": {
"psr-4": { "App\\Tests\\": "tests/" }
},
"scripts": {
"test": "phpunit tests",
"analizar": "phpstan analyse",
"estilo": "php-cs-fixer fix --dry-run --diff",
"comprobar": ["@estilo", "@analizar", "@test"]
},
"config": { "sort-packages": true }
}
composer dump-autoload
Paso 2: el modelo
<?php
// src/Modelo/Producto.php
declare(strict_types=1);
namespace App\Modelo;
use InvalidArgumentException;
final readonly class Producto
{
public function __construct(
public string $sku,
public string $nombre,
public float $precio,
public float $iva = 21.0,
) {
if ($precio < 0) {
throw new InvalidArgumentException("Precio negativo en $sku");
}
}
}
<?php
// src/Modelo/LineaPedido.php
declare(strict_types=1);
namespace App\Modelo;
final readonly class LineaPedido
{
public function __construct(
public Producto $producto,
public int $cantidad,
) {}
}
<?php
// src/Modelo/Pedido.php
declare(strict_types=1);
namespace App\Modelo;
use InvalidArgumentException;
final class Pedido
{
/** @var list<LineaPedido> */
public private(set) array $lineas = [];
public function anadir(Producto $producto, int $cantidad = 1): static
{
if ($cantidad < 1) {
throw new InvalidArgumentException('La cantidad debe ser al menos 1');
}
$this->lineas[] = new LineaPedido($producto, $cantidad);
return $this;
}
}
Paso 3: funciones de utilidad y servicio
<?php
// src/Util/funciones.php
declare(strict_types=1);
namespace App\Util;
function euros(float $cantidad): string
{
return number_format($cantidad, 2, ',', '.') . ' €';
}
<?php
// src/Servicio/CalculadoraPedido.php
declare(strict_types=1);
namespace App\Servicio;
use App\Modelo\LineaPedido;
use App\Modelo\Pedido;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
final class CalculadoraPedido
{
public const float ENVIO = 4.95;
public const float ENVIO_GRATIS_DESDE = 50.0;
public function __construct(
private readonly LoggerInterface $logger = new NullLogger(),
) {}
public function subtotal(Pedido $pedido): float
{
return round(array_sum(array_map(
fn(LineaPedido $l): float => $l->producto->precio * (1 + $l->producto->iva / 100) * $l->cantidad,
$pedido->lineas,
)), 2);
}
public function envio(Pedido $pedido): float
{
return $this->subtotal($pedido) >= self::ENVIO_GRATIS_DESDE ? 0.0 : self::ENVIO;
}
public function total(Pedido $pedido): float
{
$total = round($this->subtotal($pedido) + $this->envio($pedido), 2);
$this->logger->info('Total calculado', ['lineas' => count($pedido->lineas), 'total' => $total]);
return $total;
}
}
Fíjate en que el servicio depende de Psr\Log\LoggerInterface (el paquete psr/log lo instaló Composer como dependencia de Monolog) y usa NullLogger (un logger que no hace nada) como valor por defecto.
Paso 4: los puntos de entrada
<?php
// bin/pedido.php → php bin/pedido.php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
use App\Modelo\Pedido;
use App\Modelo\Producto;
use App\Servicio\CalculadoraPedido;
use Dotenv\Dotenv;
use Monolog\Handler\StreamHandler;
use Monolog\Level;
use Monolog\Logger;
use function App\Util\euros;
Dotenv::createImmutable(dirname(__DIR__))->safeLoad(); // safeLoad: no falla si no hay .env
$logger = new Logger($_ENV['APP_NOMBRE'] ?? 'tienda');
$logger->pushHandler(new StreamHandler(dirname(__DIR__) . '/var/log/app.log', Level::Debug));
$calculadora = new CalculadoraPedido($logger);
$pedido = new Pedido()
->anadir(new Producto('CAF-01', 'Café en grano 1 kg', 14.90, 10), 2)
->anadir(new Producto('TAZ-01', 'Taza de cerámica', 6.60));
echo 'Subtotal: ' . euros($calculadora->subtotal($pedido)) . PHP_EOL; // 40,77 €
echo 'Envío: ' . euros($calculadora->envio($pedido)) . PHP_EOL; // 4,95 €
echo 'Total: ' . euros($calculadora->total($pedido)) . PHP_EOL; // 45,72 €
<?php
// public/index.php → php -S localhost:8000 -t public
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
use App\Modelo\Pedido;
use App\Modelo\Producto;
use App\Servicio\CalculadoraPedido;
use function App\Util\euros;
$pedido = new Pedido()->anadir(new Producto('LIB-01', 'Libro de PHP 8.5', 39.90, 4));
$total = new CalculadoraPedido()->total($pedido);
?>
<!DOCTYPE html>
<html lang="es">
<head><meta charset="UTF-8"><title>Tienda</title></head>
<body>
<h1>Total del pedido: <?= htmlspecialchars(euros($total)) ?></h1>
</body>
</html>
Paso 5: una prueba
<?php
// tests/Servicio/CalculadoraPedidoTest.php
declare(strict_types=1);
namespace App\Tests\Servicio;
use App\Modelo\Pedido;
use App\Modelo\Producto;
use App\Servicio\CalculadoraPedido;
use PHPUnit\Framework\TestCase;
final class CalculadoraPedidoTest extends TestCase
{
public function testCobraEnvioPorDebajoDelMinimo(): void
{
$pedido = new Pedido()->anadir(new Producto('A', 'Barato', 10.0, 0));
self::assertSame(4.95, new CalculadoraPedido()->envio($pedido));
self::assertSame(14.95, new CalculadoraPedido()->total($pedido));
}
public function testEnvioGratisDesdeCincuentaEuros(): void
{
$pedido = new Pedido()->anadir(new Producto('B', 'Caro', 50.0, 0));
self::assertSame(0.0, new CalculadoraPedido()->envio($pedido));
}
}
composer test
# OK (2 tests, 3 assertions)
composer comprobar
# Ejecuta estilo, análisis estático y pruebas
En el módulo 18 aprenderás PHPUnit en profundidad. Lo importante aquí es ver cómo encajan las piezas: Composer gestiona dependencias y autoload, el namespace de cada clase coincide con su carpeta, las pruebas viven en tests/ con su propio namespace, y las herramientas de calidad se lanzan con un solo comando.
💡 Consejo: Este esqueleto (
src/,tests/,public/,bin/,var/,vendor/) es el mismo que usan Symfony y la mayoría de proyectos PHP profesionales. Si te acostumbras a él ahora, te sentirás como en casa en cualquier framework.
13. Errores comunes
| Error | Causa | Solución |
|---|---|---|
Class "App\Modelo\Producto" not found | Ruta o mayúsculas que no coinciden con el namespace, o falta vendor/autoload.php | Revisa carpeta, nombre de fichero y composer dump-autoload |
Class "App\Servicio\DateTime" not found | Clase global usada sin \ ni use en un fichero con namespace | use DateTime; o new \DateTime() |
Cannot declare class X, because the name is already in use | Dos use con el mismo nombre corto o un require duplicado | Usa as para el alias; deja que el autoloader cargue |
Call to undefined function App\Util\euros() | El fichero de funciones no está en autoload.files | Añádelo y ejecuta composer dump-autoload |
Your requirements could not be resolved | Restricciones de versión incompatibles entre paquetes o con tu PHP | Lee el mensaje: dice qué paquete exige qué; relaja restricciones o actualiza |
| En otro ordenador fallan cosas que en el tuyo no | No se subió composer.lock o se ejecutó update en lugar de install | Versiona el lock y usa composer install |
vendor/ en el repositorio | Falta en .gitignore | Añádelo y bórralo del índice: git rm -r --cached vendor |
| Funciona en Windows y falla en Linux | Mayúsculas distintas entre namespace y carpeta | Respeta exactamente las mayúsculas |
14. Ejercicios
Ejercicio 1: Namespaces sin Composer
Crea un mini proyecto con las clases App\Modelo\Libro, App\Modelo\Autor y App\Servicio\Biblioteca en sus carpetas correspondientes y un autoloader propio con spl_autoload_register() (sección 5). Añade una clase Externo\Modelo\Libro en otra carpeta y úsala en el mismo fichero que la tuya mediante un alias con as. Añade un segundo autoloader para el namespace Externo\.
Ejercicio 2: Migrar a Composer
Convierte el proyecto del ejercicio 1 a Composer: crea el composer.json con composer init, configura PSR-4 para App\ y Externo\, elimina tu autoloader y usa vendor/autoload.php. Añade un fichero de funciones src/Util/helpers.php en autoload.files e impórtalas con use function.
Ejercicio 3: Restricciones de versión
Para cada restricción, escribe el rango de versiones que acepta y di si aceptaría la versión indicada: ^2.3 (2.9.1, 3.0.0), ~2.3 (2.9.0), ~2.3.1 (2.4.0), ^0.7.2 (0.8.0), >=1.4 <2.0 (1.10.3), ^1.0 || ^2.0 (2.5.0). Después compruébalo con la calculadora de restricciones de https://semver.madewithlove.com/.
Ejercicio 4: Configuración y logs
En el proyecto de ejemplo, crea un .env y un .env.example con APP_ENV, APP_DEBUG y LOG_NIVEL. Haz que el nivel del logger de Monolog dependa de LOG_NIVEL (pista: Level::fromName()), que en APP_ENV=desarrollo los logs también salgan por php://stderr, y que el script falle con un mensaje claro si falta alguna variable obligatoria.
Ejercicio 5: Cliente de una API pública
Crea una clase App\Servicio\ClienteTiempo que reciba un GuzzleHttp\Client por el constructor y tenga un método temperaturaActual(float $lat, float $lon): float que consulte una API meteorológica pública y gratuita sin clave (por ejemplo, Open-Meteo). Muestra la temperatura de tres ciudades españolas con un script en bin/. Gestiona los errores de red con GuzzleException.
Ejercicio 6: Calidad automática
Añade al proyecto PHP-CS-Fixer con @PER-CS y PHPStan en nivel 8. Introduce a propósito tres errores de estilo y dos de tipos (por ejemplo, devolver null en un método que promete float) y comprueba que las herramientas los detectan. Corrígelos y crea un script de Composer comprobar que ejecute estilo, análisis y pruebas.
Ejercicio 7: Tu propia librería
Crea un paquete tu-nombre/slugger de tipo library con una clase TuNombre\Slugger\Slugger (método slug(string $texto): string implementado con el operador pipe) y su prueba. En otro proyecto, instálalo desde tu carpeta local usando un repositorio de tipo path en composer.json ("repositories": [{"type": "path", "url": "../slugger"}]) y úsalo.
Resumen
| Concepto | Sintaxis / comando |
|---|---|
| Declarar namespace | namespace App\Modelo; |
| Nombre completo | \App\Modelo\Usuario, Usuario::class |
| Importar clase | use App\Modelo\Usuario; |
| Alias | use Pagos\Usuario as UsuarioPagos; |
| Importar función / constante | use function App\Util\slugify; / use const App\Util\SEPARADOR; |
| Agrupado | use App\Modelo\{Usuario, Producto}; |
| Clase global en namespace | new \DateTimeImmutable() o use DateTimeImmutable; |
| Autoload manual | spl_autoload_register(fn(string $clase) => ...) |
| Crear proyecto | composer init |
| Añadir dependencia | composer require vendor/paquete / --dev |
| Instalar desde el lock | composer install |
| Actualizar versiones | composer update |
| Regenerar autoload | composer dump-autoload |
| Autoload Composer | require 'vendor/autoload.php'; |
| PSR-4 | "autoload": {"psr-4": {"App\\": "src/"}} |
| Restricción habitual | ^3.9 = >=3.9.0 <4.0.0 |
| Seguridad | composer audit |
| Estilo | vendor/bin/php-cs-fixer fix con @PER-CS |
| Análisis estático | vendor/bin/phpstan analyse |