Ir al contenido principal

Devlog · Fase 0 · 5/7

La arquitectura en Módulos del repo web

Cinco módulos, un único punto de salida hacia el cloud y revisiones que se leen en la ruta del archivo: la arquitectura Laravel del repositorio web, generado en más de un noventa por ciento por agentes de IA.

Publicado el 10 agosto 20265 min de lectura
  • laravel
  • architecture
  • octane

Un sitio web sin base de datos son unas cuantas páginas y un formulario. La tentación, en un perímetro tan pequeño, es amontonarlo todo en dos o tres controllers y pasar a otra cosa. Tomé el camino contrario desde la primera semana del proyecto. Cuando puse la landing en abril, ordené cada responsabilidad en su propio módulo. Hoy el repositorio tiene cinco dominios: Cloud, Locale, Seo, Waitlist y ahora Blog. Por eso esta disciplina se sostiene, incluso a esta escala.

Una carpeta por dominio, no por tipo técnico

La estructura de partida de Laravel ordena el código por naturaleza: todos los controllers juntos, todos los servicios juntos, todas las clases de utilidad juntas. En aubia.dev, pasé a un orden por dominio de negocio. Cada carpeta de app/Modules/ lleva una intención de producto completa, con sus subcarpetas Actions/, DataTransferObjects/, Http/ y Enums/ cuando son útiles.

  • Cloud: el único punto de salida HTTP hacia api.aubia.cloud.
  • Locale: la resolución del idioma antes del enrutamiento.
  • Seo: la generación del sitemap y las señales de indexación.
  • Waitlist: el proxy de registro con double opt-in.
  • Blog: el motor flat-file que sirve este artículo.

Cuando una intención cambia, sé exactamente dónde mirar. Ningún archivo juega en dos bandos.

El mapa animado de los módulos del repositorio web: Locale, Seo, Waitlist y Blog convergen hacia el módulo Cloud, punto de salida único hacia api.aubia.cloud.

Las Actions: una intención, una clase

Cada gesto de negocio vive en su propia clase Action. Ningún servicio cajón de sastre que acumule quince métodos con el paso de los meses: una clase hace una cosa, la expone mediante un método de entrada, y se prueba fuera de la stack HTTP. La convención de Aubia quiere estas clases final readonly por defecto, con un punto de entrada con nombre (execute) o invocable.

La resolución del locale es un buen ejemplo. Repite la misma cascada que el middleware, sin tocar el segmento de la URL, aislada para seguir siendo testeable:

public function __invoke(Request $request): string
{
    $candidates = [
        $request->cookie(SetLocale::COOKIE_NAME),
        $this->extractAcceptLanguage($request),
        config('app.locale'),
    ];

    foreach ($candidates as $candidate) {
        if (is_string($candidate) && in_array($candidate, SetLocale::SUPPORTED_LOCALES, true)) {
            return $candidate;
        }
    }

    return SetLocale::DEFAULT_LOCALE;
}

El controller que llama a esta Action queda trivial: delega y redirige. Toda la lógica testeable está en otra parte.

Los DTO readonly: un contrato tipado entre las capas

Entre una capa y la siguiente, nunca hago transitar un array asociativo tipado a la ligera. Cada payload estructurada pasa por un Data Transfer Object en PHP puro: una readonly class con propiedades promovidas, con un fromArray() a la entrada y un método de salida explícito. Ninguna dependencia externa para eso, ningún mapeo mágico: el tipado estricto de PHP 8.5 basta, y el contrato se lee de un vistazo.

El registro en la lista de espera ilustra por qué este contrato importa. El formulario captura un campo honeypot temporal, startedAt, usado solo para atrapar bots del lado de la validación. Este campo nunca debe salir hacia el cloud. El DTO lo lleva internamente pero lo excluye de la payload saliente:

public function toCloudPayload(): array
{
    return [
        'email' => $this->email,
        'utm_source' => $this->utmSource,
        'utm_medium' => $this->utmMedium,
        'utm_campaign' => $this->utmCampaign,
        'locale' => $this->locale,
    ];
}

La frontera entre lo que la web conoce y lo que el cloud recibe está escrita con todas las letras, en un método cuyo nombre dice exactamente su función.

Un único punto de salida hacia el cloud

Dado que la Fase 0 no almacena nada en local, todo dato persistente sale hacia el repositorio cloud mediante un proxy HTTP. Centralicé estas llamadas en una sola clase, CloudApiClient, para que las Actions nunca tengan que conocer los detalles del cliente Http nativo de Laravel: timeouts, backoff, logging sin dato personal. El cliente aplica dos políticas de reintento distintas según la idempotencia del verbo. Un GET se reintenta ante un error de red y ante un 5xx, sin efecto secundario. Un POST firmado solo se reintenta ante un fallo de red rotundo, nunca ante una respuesta recibida, para no arriesgar un doble registro.

Este cliente es final pero deliberadamente no readonly a nivel de clase: bajo Octane, debe permanecer sin estado de una petición a otra, y seguir siendo mockeable en los tests. Un detalle de disciplina stateless que evita las fugas entre workers.

Por qué esta disciplina en un repo tan pequeño

La pregunta honesta es: ¿no es demasiado para cinco páginas?

Este repositorio lo generan en un noventa por ciento y más los agentes de IA, y la revisión humana sigue siendo el verdadero cuello de botella. Una frontera de módulo nítida es un guardarraíl en ambos sentidos: un agente soltado sobre Waitlist no se desborda hacia Seo por accidente, y mis revisiones quedan cortas de leer porque el perímetro de un cambio se adivina en su ruta de archivo.

La Fase 1 traerá un verdadero sitio de producto, y cada módulo de hoy es un cimiento, no un andamiaje desechable. Y esta estructura es coherente de un repositorio de Aubia al siguiente: encuentro los mismos reflejos en el cockpit desktop y en el cloud. Esa regularidad, en un proyecto llevado en solitario, bien vale unas cuantas carpetas de más.

Meterse bajo la piel del sitio

Queda lo más visible: la paleta, el cristal y el movimiento. Para recibir la invitación a la beta 0.1 en cuanto se abra, la lista de espera está ahí.

Esta obra se cuenta aquí, artículo a artículo. Súbase a bordo: sus comentarios dibujarán lo que viene.

Únase a la lista de espera