Guía completa de PHP moderno para desarrolladores: buenas prácticas, rendimiento y seguridad

php Guía completa de PHP moderno para desarrolladores: buenas prácticas, rendimiento y seguridad

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.

Comentarios
¿Quieres comentar?

Inicia sesión con Telegram para participar en la conversación


Comentarios (0)

Aún no hay comentarios. ¡Sé el primero en comentar!

Iniciar Sesión