Guía completa de PHP moderno para desarrolladores
Esta guía muestra lo esencial para construir aplicaciones PHP modernas: estructura de proyecto, autoloading PSR-4, manejo de dependencias con Composer, buenas prácticas de tipado y OOP, seguridad, rendimiento y un ejemplo práctico —una API REST de tareas (TODO)— usando FastRoute y PDO. Incluye por qué se hace así y pasos concretos para ponerlo en marcha.
Requisitos
- PHP >= 8.1 (8.2+ recomendado)
- Composer
- MySQL/Postgres o SQLite para pruebas
- Opcional: Xdebug, PHPStan/Psalm, PHPUnit
Configuración rápida
Instala PHP y Composer. Para desarrollo local, el servidor embebido es suficiente:
php -S localhost:8000 -t public
Estructura sugerida de proyecto
todo-api/
├─ public/
│ └─ index.php
├─ src/
│ ├─ Controllers/
│ ├─ Models/
│ ├─ Database/
│ └─ Utils/
├─ tests/
├─ vendor/
├─ composer.json
├─ .env
└─ migrations/
composer.json (mínimo)
{
"name": "my/todo-api",
"require": {
"php": "^8.1",
"nikic/fast-route": "^1.3",
"vlucas/phpdotenv": "^5.5",
"monolog/monolog": "^2.9"
},
"autoload": {
"psr-4": {
"App\\\\": "src/"
}
},
"require-dev": {
"phpunit/phpunit": "^9.5"
}
}
Ejecuta composer install. PSR-4 te da autoload limpio y mantenible.
Archivo .env (ejemplo)
DB_DSN=mysql:host=127.0.0.1;dbname=todo;charset=utf8mb4
DB_USER=root
DB_PASS=secret
APP_ENV=development
Conexión a DB: src/Database/Connection.php
<?php
namespace App\Database;
use PDO;
use PDOException;
final class Connection
{
private PDO $pdo;
public function __construct(string $dsn, string $user, string $pass)
{
try {
$this->pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
} catch (PDOException $e) {
// aquí podrías loggear y lanzar una excepción personalizada
throw $e;
}
}
public function pdo(): PDO
{
return $this->pdo;
}
}
Modelo sencillo: src/Models/Todo.php
<?php
namespace App\Models;
class Todo
{
public function __construct(
public ?int $id,
public string $title,
public bool $completed = false
) {}
public static function fromArray(array $data): self
{
return new self(
$data['id'] ?? null,
$data['title'],
isset($data['completed']) ? (bool)$data['completed'] : false
);
}
public function toArray(): array
{
return [
'id' => $this->id,
'title' => $this->title,
'completed' => $this->completed,
];
}
}
Controlador: src/Controllers/TodoController.php
<?php
namespace App\Controllers;
use App\Database\Connection;
use App\Models\Todo;
use PDO;
final class TodoController
{
private PDO $db;
public function __construct(Connection $connection)
{
$this->db = $connection->pdo();
}
public function list(): array
{
$stmt = $this->db->query('SELECT id, title, completed FROM todos');
$rows = $stmt->fetchAll();
return array_map(fn($r) => Todo::fromArray($r)->toArray(), $rows);
}
public function create(array $data): Todo
{
// Validación mínima: siempre valida y sanitiza
if (empty(trim($data['title'] ?? ''))) {
throw new \InvalidArgumentException('title required');
}
$stmt = $this->db->prepare('INSERT INTO todos (title, completed) VALUES (:title, :completed)');
$stmt->execute([
':title' => $data['title'],
':completed' => $data['completed'] ?? 0,
]);
$id = (int)$this->db->lastInsertId();
return new Todo($id, $data['title'], (bool)($data['completed'] ?? false));
}
// update / delete similares
}
Router y bootstrap: public/index.php
<?php
require __DIR__ . '/../vendor/autoload.php';
use Dotenv\Dotenv;
use App\Database\Connection;
use App\Controllers\TodoController;
use FastRoute\RouteCollector;
Dotenv::createImmutable(__DIR__ . '/../')->load();
$dsn = getenv('DB_DSN');
$user = getenv('DB_USER');
$pass = getenv('DB_PASS');
$connection = new Connection($dsn, $user, $pass);
$controller = new TodoController($connection);
$dispatcher = FastRoute\simpleDispatcher(function(RouteCollector $r) {
$r->addRoute('GET', '/todos', 'list');
$r->addRoute('POST', '/todos', 'create');
});
// basic request parsing
$httpMethod = $_SERVER['REQUEST_METHOD'];
$uri = rawurldecode(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH));
$routeInfo = $dispatcher->dispatch($httpMethod, $uri);
switch ($routeInfo[0]) {
case FastRoute\Dispatcher::NOT_FOUND:
http_response_code(404);
echo json_encode(['error' => 'Not found']);
break;
case FastRoute\Dispatcher::METHOD_NOT_ALLOWED:
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
break;
case FastRoute\Dispatcher::FOUND:
$handler = $routeInfo[1];
// simple mapping
try {
if ($handler === 'list') {
header('Content-Type: application/json');
echo json_encode($controller->list());
} elseif ($handler === 'create') {
$input = json_decode(file_get_contents('php://input'), true) ?? [];
$todo = $controller->create($input);
http_response_code(201);
header('Content-Type: application/json');
echo json_encode($todo->toArray());
}
} catch (\Throwable $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
break;
}
Migración simple
CREATE TABLE todos (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
completed TINYINT(1) DEFAULT 0
);
Buenas prácticas y por qué
- Tipado y constructores promocionados: aumentan la autocompletación y detectan errores temprano.
- PSR-4: evita includes manuales y facilita la organización por dominio.
- Prepared statements: previenen inyección SQL.
- Dotenv: no pongas secretos en el repo; usa variables de entorno.
- Separa responsabilidades: controllers del acceso a datos, DTOs para transporte.
Seguridad esencial
- Siempre usar prepared statements o query builders.
- Para autenticación, usa password_hash/password_verify con PASSWORD_DEFAULT.
- Protección CSRF en formularios; tokens por sesión.
- Valida y normaliza entrada: nunca confíes en datos del cliente.
- Configura cabeceras de seguridad (Content-Security-Policy, X-Frame-Options, etc.).
- Limita exposición de errores en producción: muestra mensajes amigables y loggea internamente.
Rendimiento
- Activa OPcache (en php.ini): aumenta dramáticamente el rendimiento.
- Usa cache para consultas costosas (Redis, Memcached).
- Evita N+1 queries; usa joins o batch queries.
- Profilea con Xdebug o Blackfire antes de optimizar por intuición.
- Considera preloading en PHP 8.1+ para cargar clases comunes en el proceso.
Testing y CI
Escribe pruebas unitarias y de integración con PHPUnit. Un workflow básico de GitHub Actions ejecuta tests en cada push.
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
- run: composer install --prefer-dist --no-progress --no-interaction
- run: vendor/bin/phpunit --configuration phpunit.xml
Errores comunes
- No validar entrada (leading to injections/logic bugs).
- Exponer errores detallados en producción.
- Ignorar configuración de OPcache y conexión persistente DB cuando hace falta.
- Usar funciones deprecated en nuevas versiones de PHP.
Siguientes pasos recomendados
1) Añade autenticación JWT y control de permisos. 2) Introduce un ORM o query builder si tu dominio lo requiere (Eloquent, Doctrine). 3) Habilita el profiling, escribe tests de integración y configura despliegue automatizado.
Consejo avanzado: cuando la latencia y la concurrencia sean críticas, considera usar RoadRunner o Swoole para aplicaciones PHP persistentes; cambian el paradigma de ciclo de vida y aumentan el rendimiento, pero requieren gestión explícita de estado y limpieza de recursos entre requests.
Advertencia: antes de actualizar a una versión mayor de PHP en producción, ejecuta un chequeo completo con PHPStan/Psalm y corre la suite de tests para detectar incompatibilidades.
¿Quieres comentar?
Inicia sesión con Telegram para participar en la conversación