Módulos, ruteo y secuencia de arranque¶
Secuencia de arranque¶
bootstrap/app.php arranca en 9 etapas ordenadas:
| Etapa | Qué pasa |
|---|---|
| 1 | Carga .env, valida las variables requeridas |
| 2 | Setea la ruta de config |
| 3 | Setea el error reporting, desactiva display_errors |
| 4 | Construye el Container PSR-11 |
| 5 | Registra el singleton del Logger + el manejador global de excepciones |
| 6 | Registra los singletons PDO, DB y CacheInterface (ApcuCache) |
| 7 | Registra los singletons Router + Kernel |
| 7.5 | Registra el EventDispatcher; autodescubre + arranca los ServiceProviders de los módulos |
| 8 | Autodescubre los routes.php de los módulos |
| 9 | Registra las rutas de infraestructura (health, logs) |
La etapa 7.5 llama register() y luego boot() en cada app/Modules/*/ServiceProvider.php que exista. Ahí es donde los módulos se suscriben a eventos y sobrescriben bindings del container.
Crear un módulo¶
Repository¶
namespace App\Modules\Producto\Repositories;
use PDO;
use App\Exceptions\NotFoundException;
class ProductoRepository
{
public function __construct(private PDO $pdo) {}
/** @return list<array<string, mixed>> */
public function findAll(): array
{
return $this->pdo->query('SELECT * FROM productos')
->fetchAll(PDO::FETCH_ASSOC);
}
/** @return array<string, mixed> */
public function findById(int $id): array
{
$stmt = $this->pdo->prepare('SELECT * FROM productos WHERE id = ?');
$stmt->execute([$id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row) {
throw new NotFoundException('Producto', $id);
}
return $row;
}
/** @return array<string, mixed> */
public function create(array $data): array
{
$stmt = $this->pdo->prepare('INSERT INTO productos (nombre, precio) VALUES (?, ?)');
$stmt->execute([$data['nombre'], $data['precio']]);
return $this->findById((int) $this->pdo->lastInsertId());
}
}
Service¶
namespace App\Modules\Producto\Services;
use App\Modules\Producto\Repositories\ProductoRepository;
class ProductoService
{
public function __construct(private ProductoRepository $repository) {}
public function getAll(): array { return $this->repository->findAll(); }
public function get(int $id): array { return $this->repository->findById($id); }
public function create(array $d): array { return $this->repository->create($d); }
}
Controller¶
namespace App\Modules\Producto\Controllers;
use App\Support\Request;
use App\Support\Response;
use App\Modules\Producto\Services\ProductoService;
use App\Modules\Producto\Requests\CreateProductoRequest;
class ProductoController
{
public function __construct(private ProductoService $service) {}
public function index(Request $request): Response
{
return Response::success($this->service->getAll());
}
public function show(Request $request): Response
{
return Response::success($this->service->get((int) $request->route('id')));
}
public function create(CreateProductoRequest $request): Response
{
return Response::success($this->service->create($request->validated()), 201);
}
}
ServiceProvider¶
namespace App\Modules\Producto;
use App\Support\ServiceProvider;
use App\Modules\Producto\Repositories\ProductoRepository;
use App\Modules\Producto\Services\ProductoService;
use App\Modules\Producto\Controllers\ProductoController;
class ProductoServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->container->singleton(ProductoRepository::class, fn ($c) =>
new ProductoRepository($c->get(\PDO::class))
);
$this->container->singleton(ProductoService::class, fn ($c) =>
new ProductoService($c->get(ProductoRepository::class))
);
$this->container->singleton(ProductoController::class, fn ($c) =>
new ProductoController($c->get(ProductoService::class))
);
}
public function boot(): void
{
$router = $this->container->get(\App\Support\Router::class);
require __DIR__ . '/routes.php';
}
}
Ruteo¶
Rutas individuales¶
// Pública
$router->post('/auth/login', [AuthController::class, 'login']);
// Con middlewares
$router->get('/productos/{id}', [ProductoController::class, 'show'],
[AuthMiddleware::class]);
$router->delete('/productos/{id}', [ProductoController::class, 'delete'],
[AuthMiddleware::class, AdminMiddleware::class]);
Grupos de rutas — comparten middlewares y prefijo de URI¶
// Grupo por middleware
$router->group([AuthMiddleware::class], function ($router) {
$router->get('/productos', [ProductoController::class, 'index']);
$router->post('/productos', [ProductoController::class, 'create']);
$router->put('/productos/{id}', [ProductoController::class, 'update']);
});
// Grupo con prefijo — a todas las rutas se les antepone /v1
$router->group([AuthMiddleware::class], '/v1', function ($router) {
$router->get('/productos', [ProductoController::class, 'index']); // → GET /v1/productos
});
// Grupos anidados — los middlewares y prefijos se combinan, no se reemplazan
$router->group([AuthMiddleware::class], function ($router) {
$router->group([AdminMiddleware::class], function ($router) {
$router->get('/admin/roles', [AdminController::class, 'roles']);
});
});
// Middleware parametrizado — chequeo de permiso RBAC
$router->delete('/productos/{id}', [ProductoController::class, 'delete'], [
AuthMiddleware::class,
PermissionMiddleware::class . ':productos.delete',
]);
Los parámetros de ruta se extraen automáticamente y están disponibles vía $request->route('id').
Inyección en controllers¶
El router resuelve los parámetros de los métodos del controller por tipo:
| Tipo del parámetro | Qué se inyecta |
|---|---|
Request |
La petición actual (con user(), tenantId(), params de ruta ya seteados) |
Subclase de FormRequest |
Una instancia nueva construida desde $request->all() + routeParams() — validada al construirse |
| Cualquier otra clase | Se resuelve desde el container |
| Escalar (sin tipo) | $request->route($paramName) |
Patrón de doble parámetro — usalo cuando necesitás tanto el contexto de la petición (tenantId, user) como un FormRequest validado:
public function create(Request $request, CreateProductoRequest $validated): Response
{
$tenantId = (string) $request->tenantId();
return Response::success($this->service->create($validated->validated(), $tenantId), 201);
}