Skip to content

Módulos, ruteo y secuencia de arranque

Boot sequence

bootstrap/app.php boots in 9 ordered stages:

Stage What happens
1 Load .env, enforce required vars
2 Set config path
3 Set error reporting, disable display_errors
4 Build PSR-11 Container
5 Register Logger singleton + global exception handler
6 Register PDO, DB, and CacheInterface (ApcuCache) singletons
7 Register Router + Kernel singletons
7.5 Register EventDispatcher; auto-discover + boot module ServiceProviders
8 Auto-discover module routes.php files
9 Register infrastructure routes (health, logs)

Stage 7.5 calls register() then boot() on every app/Modules/*/ServiceProvider.php that exists. This is where modules subscribe to events and override container bindings.


Creating a module

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';
    }
}

Routing

Individual routes

// Public
$router->post('/auth/login', [AuthController::class, 'login']);

// With middlewares
$router->get('/productos/{id}', [ProductoController::class, 'show'],
    [AuthMiddleware::class]);

$router->delete('/productos/{id}', [ProductoController::class, 'delete'],
    [AuthMiddleware::class, AdminMiddleware::class]);

Route groups — share middlewares and URI prefix

// Middleware group
$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']);
});

// Prefix group — all routes get /v1 prepended
$router->group([AuthMiddleware::class], '/v1', function ($router) {
    $router->get('/productos', [ProductoController::class, 'index']); // → GET /v1/productos
});

// Nested groups — middlewares and prefixes are merged, not replaced
$router->group([AuthMiddleware::class], function ($router) {
    $router->group([AdminMiddleware::class], function ($router) {
        $router->get('/admin/roles', [AdminController::class, 'roles']);
    });
});

// Parameterized middleware — RBAC permission check
$router->delete('/productos/{id}', [ProductoController::class, 'delete'], [
    AuthMiddleware::class,
    PermissionMiddleware::class . ':productos.delete',
]);

Route parameters are extracted automatically and available via $request->route('id').

Controller injection

The router resolves controller method parameters by type:

Parameter type What gets injected
Request The current request (with user(), tenantId(), route params already set)
Subclass of FormRequest A new instance constructed from $request->all() + routeParams() — validated on construction
Any other class Resolved from the container
Scalar (untyped) $request->route($paramName)

Dual-parameter pattern — use when you need both the request context (tenantId, user) and a validated FormRequest:

public function create(Request $request, CreateProductoRequest $validated): Response
{
    $tenantId = (string) $request->tenantId();
    return Response::success($this->service->create($validated->validated(), $tenantId), 201);
}