# Regla: Patrones de Arquitectura — PHP / Laravel

Aplica a todo código PHP en proyectos Laravel. Estos patrones separan las
responsabilidades entre capas, facilitan el testing y reducen el acoplamiento.
Un controller gordo que contiene lógica de negocio y queries es deuda técnica
inmediata — esta regla establece cómo evitarla.

---

## Service Container y Dependency Injection

- Registrar toda clase de negocio en el Service Container de Laravel.
- Inyectar dependencias por constructor, nunca instanciarlas dentro de la clase.
- Bind interfaces a implementaciones concretas en `AppServiceProvider`:

```php
// AppServiceProvider::register()
$this->app->bind(FacturaRepositoryInterface::class, EloquentFacturaRepository::class);
```

- En producción, usar `$this->app->singleton()` para servicios sin estado.

---

## Facades: con criterio

- Las Facades de Laravel son cómodas pero dificultan el testing cuando se usan
  en lógica de negocio profunda.
- Facades **aceptables** en controllers y middleware (capa de entrada):
  `Auth::user()`, `Cache::get()`, `Log::error()`.
- Facades **prohibidas** en Services, Repositories y Jobs: inyectar la interfaz
  correspondiente para que sea mockeable en tests.

```php
// MAL — Facade en service, imposible de testear sin mock estático
class FacturaService
{
    public function emitir(int $facturaId): void
    {
        $factura = Factura::find($facturaId); // Facade implícita de Eloquent
        Mail::send(...);                       // Facade de Mail no inyectada
    }
}

// BIEN — dependencias inyectadas
class FacturaService
{
    public function __construct(
        private readonly FacturaRepositoryInterface $facturas,
        private readonly MailerInterface $mailer,
    ) {}

    public function emitir(int $facturaId): void
    {
        $factura = $this->facturas->findOrFail($facturaId);
        $this->mailer->enviarFactura($factura);
    }
}
```

---

## Repository Pattern sobre Eloquent directo en controllers

- Los controllers no deben contener queries Eloquent.
- Definir una interfaz del repositorio y una implementación Eloquent:

```php
// Interfaz
interface FacturaRepositoryInterface
{
    public function findOrFail(int $id): Factura;
    /** @return Collection<int, Factura> */
    public function listarPorEmpresa(int $empresaId, EstatusFactura $estatus): Collection;
}

// Implementación
class EloquentFacturaRepository implements FacturaRepositoryInterface
{
    public function findOrFail(int $id): Factura
    {
        return Factura::with(['items', 'cliente'])->findOrFail($id);
    }

    public function listarPorEmpresa(int $empresaId, EstatusFactura $estatus): Collection
    {
        return Factura::where('empresa_id', $empresaId)
            ->where('estatus', $estatus)
            ->with('cliente')
            ->orderByDesc('fecha_emision')
            ->get();
    }
}
```

---

## Form Requests para validación

- Toda validación de entrada HTTP va en una clase `FormRequest`, nunca en el controller.
- Las reglas de autorización también van en el `FormRequest`:

```php
class EmitirFacturaRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('emitir-facturas');
    }

    public function rules(): array
    {
        return [
            'cliente_id'  => ['required', 'integer', 'exists:clientes,id'],
            'items'       => ['required', 'array', 'min:1'],
            'items.*.sku' => ['required', 'string', 'max:50'],
            'items.*.qty' => ['required', 'integer', 'min:1'],
        ];
    }
}
```

---

## Events y Listeners para desacoplamiento

- Acciones con efectos secundarios múltiples (enviar email, actualizar reporte,
  notificar terceros) se publican como eventos, no se llaman directamente:

```php
// En el service — solo dispara el evento
event(new FacturaEmitida($factura));

// Listeners separados, registrados en EventServiceProvider
FacturaEmitida::class => [
    EnviarEmailFacturaListener::class,
    GenerarPdfFacturaListener::class,
    NotificarSatListener::class,
],
```

---

## Jobs y Queues para procesamiento asíncrono

- Todo proceso que tarde más de 500ms o que pueda fallar y reintentarse va en un Job:

```php
class GenerarReporteFacturacionJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;
    public int $backoff = 60; // segundos entre reintentos

    public function __construct(private readonly int $empresaId) {}

    public function handle(ReporteService $reportes): void
    {
        $reportes->generarFacturacionMensual($this->empresaId);
    }
}

// Despachar desde el controller
GenerarReporteFacturacionJob::dispatch($empresaId)->onQueue('reportes');
```

---

## Policies para autorización

- Las reglas de quién puede hacer qué sobre un modelo van en una Policy, no en el controller:

```php
class FacturaPolicy
{
    public function cancelar(User $user, Factura $factura): bool
    {
        return $user->empresa_id === $factura->empresa_id
            && $user->hasRole('admin')
            && ! $factura->estatus->esTerminal();
    }
}

// En el controller
$this->authorize('cancelar', $factura);
```

---

## API Resources para transformación de respuestas

- NUNCA devolver modelos Eloquent directamente en respuestas JSON.
  Un `->toArray()` de Eloquent expone todos los campos incluyendo timestamps internos.
- Usar `JsonResource` para controlar exactamente qué se expone:

```php
class FacturaResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id'            => $this->id,
            'folio'         => $this->folio,
            'total'         => $this->total,
            'estatus'       => $this->estatus,
            'fecha_emision' => $this->fecha_emision->toISOString(),
            'cliente'       => new ClienteResource($this->whenLoaded('cliente')),
            'items'         => FacturaItemResource::collection($this->whenLoaded('items')),
        ];
    }
}
```

---

## Middleware para cross-cutting concerns

- Lógica que aplica a múltiples rutas (autenticación, rate limiting, logging de auditoría,
  verificación de suscripción) va en middleware, no en cada controller.
- Nunca duplicar lógica de middleware dentro de métodos de controller.

---

## Checklist de patrones antes de merge

- [ ] Ningun controller contiene queries Eloquent directas
- [ ] Toda validación HTTP en FormRequest, no en controller
- [ ] Efectos secundarios múltiples publicados como eventos
- [ ] Procesos lentos o con reintentos en Jobs con `ShouldQueue`
- [ ] Reglas de autorización en Policies, no en controllers ni services
- [ ] Respuestas JSON devueltas con JsonResource, no con `->toArray()`
- [ ] Interfaces definidas para repositorios y servicios externos
