PHPackages                             innodite/laravel-module-maker - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. innodite/laravel-module-maker

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

innodite/laravel-module-maker
=============================

Generador de módulos Laravel con arquitectura de contextos dinámicos (Central, Shared, Tenant). Genera controladores, servicios, repositorios, migraciones e inyección de rutas con un solo comando.

v3.5.6(2mo ago)3163MITPHPPHP ^8.2CI passing

Since Aug 8Pushed 2mo agoCompare

[ Source](https://github.com/Innodite/laravel-module-maker)[ Packagist](https://packagist.org/packages/innodite/laravel-module-maker)[ Docs](https://github.com/innodite/laravel-module-maker)[ RSS](/packages/innodite-laravel-module-maker/feed)WikiDiscussions main Synced 3w ago

READMEChangelogDependencies (8)Versions (52)Used By (0)

🏗️ Innodite Laravel Module Maker
================================

[](#️-innodite-laravel-module-maker)

[![Tests](https://github.com/Innodite/laravel-module-maker/actions/workflows/tests.yml/badge.svg)](https://github.com/Innodite/laravel-module-maker/actions/workflows/tests.yml)[![Coverage](https://github.com/Innodite/laravel-module-maker/actions/workflows/coverage.yml/badge.svg)](https://github.com/Innodite/laravel-module-maker/actions/workflows/coverage.yml)[![Latest Version](https://camo.githubusercontent.com/3620cdde066afe11a00ab3b227a4169598f64134615d5baec7d4654ed19f12f7/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f762f7461672f496e6e6f646974652f6c61726176656c2d6d6f64756c652d6d616b65723f6c6162656c3d76657273696f6e26636f6c6f723d696e6469676f)](https://github.com/Innodite/laravel-module-maker/releases)[![PHP](https://camo.githubusercontent.com/38027453aeb7eb818641c9de8f82b7624c3558d92634f1946edc715c3ddf8956/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d382e332532422d3737374242343f6c6f676f3d706870266c6f676f436f6c6f723d7768697465)](https://www.php.net/)[![Laravel](https://camo.githubusercontent.com/d55faf9b2d3490c42062a679f507f4e34b4eb3cc8392ac05367d05e572ec05d8/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c61726176656c2d313125324225323025374325323031322d4646324432303f6c6f676f3d6c61726176656c266c6f676f436f6c6f723d7768697465)](https://laravel.com/)[![License](https://camo.githubusercontent.com/834000645e8f2d672b159c6e8aeff6535379ac0980be15ee000cede4aab493c6/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f496e6e6f646974652f6c61726176656c2d6d6f64756c652d6d616b65723f636f6c6f723d677265656e)](LICENSE)

**v3.5.3** — Generador de módulos Laravel con arquitectura de contextos dinámicos (Central, Shared, Tenant) para proyectos multi-tenant. Genera backend completo, inyecta rutas y crea vistas Vue 3 listas para usar — todo con un solo comando. Soporta múltiples entidades por módulo con subcarpeta aislada por entidad (`{Tipo}/{Contexto}/{Entidad}/`).

⚠️ Versiones Deprecadas
-----------------------

[](#️-versiones-deprecadas)

Se consideran **deprecados** los tags históricos con referencias heredadas a software/proyecto externo.

Tags deprecados:

- `v2.5.0`
- `v3.2.7` a `v3.4.0`

Versión mínima recomendada para uso nuevo:

- `v3.4.1+`

> Nota: la deprecación es de soporte/uso recomendado. No se reescribe el historial Git publicado.

---

📋 Tabla de Contenidos
---------------------

[](#-tabla-de-contenidos)

- [Requisitos](#-requisitos)
- [Instalación](#-instalaci%C3%B3n)
- [Tabla comparativa de contextos](#-tabla-comparativa-de-contextos)
- [Versiones Deprecadas](#-versiones-deprecadas)
- [Arquitectura Frontend](#-arquitectura-frontend)
- [Guía de comandos](#-gu%C3%ADa-de-comandos)
- [Archivos generados por contexto](#-archivos-generados-por-contexto)
- [Flujo completo por contexto](#-flujo-completo-por-contexto)
- [Composables Vue 3](#-composables-vue-3)
- [Stubs contextuales](#-stubs-contextuales)
- [Bridge Frontend-Backend](#-bridge-frontend-backend)
- [Estructura de contextos](#-estructura-de-contextos-contextsjson)
- [Estructura de árbol de un módulo generado](#-estructura-de-%C3%A1rbol-de-un-m%C3%B3dulo-generado)
- [Convenciones de nomenclatura](#-convenciones-de-nomenclatura)
- [Flujo de inyección de rutas](#-flujo-de-inyecci%C3%B3n-de-rutas)
- [Auditoría](#-auditor%C3%ADa)
- [Pruebas](#-pruebas)
- [Estándares de código](#-est%C3%A1ndares-de-c%C3%B3digo)
- [Publicar en Packagist](#-publicar-en-packagist--repositorio-privado)
- [Changelog](#-changelog)
- [Licencia](#-licencia)

**Nuevos en v3.5.x:**

- [Subcarpeta por entidad](#-subcarpeta-por-entidad-r05) — patrón `{Tipo}/{Contexto}/{Entidad}/`
- [`innodite:add-entity`](#-innoditeadd-entity--agregar-entidad-a-m%C3%B3dulo-existente) — nuevo comando para módulos multi-entidad

---

✅ Requisitos
------------

[](#-requisitos)

DependenciaVersión mínimaPHP8.2+Laravel11.0+illuminate/support^11.0|^12.0illuminate/console^11.0|^12.0illuminate/filesystem^11.0|^12.0illuminate/routing^11.0|^12.0@inertiajs/vue3^1.0 (frontend)Vue^3.0 (frontend)> Compatible opcionalmente con `stancl/tenancy` y `spatie/laravel-permission`.

---

🚀 Instalación
-------------

[](#-instalación)

```
composer require innodite/laravel-module-maker
```

Al instalar por primera vez, el paquete detecta la ausencia de configuración y sugiere el setup en consola.

### Inicializar el proyecto (requerido)

[](#inicializar-el-proyecto-requerido)

```
php artisan innodite:module-setup
```

Crea `module-maker-config/` en la raíz del proyecto con:

- `contexts.json` — Definición de contextos y tenants
- `stubs/contextual/` — Plantillas PHP y Vue personalizables

### Publicar assets manualmente

[](#publicar-assets-manualmente)

```
# Configuración make-module.php
php artisan vendor:publish --tag=module-maker-config

# Stubs PHP y Vue para personalización (4 carpetas contextuales)
php artisan vendor:publish --tag=module-maker-stubs

# contexts.json de ejemplo
php artisan vendor:publish --tag=module-maker-contexts

# Composables Vue 3 (useModuleContext, usePermissions)
php artisan vendor:publish --tag=module-maker-frontend
```

---

🗺️ Tabla comparativa de contextos
---------------------------------

[](#️-tabla-comparativa-de-contextos)

Los 4 contextos disponibles cubren todos los escenarios de un proyecto multi-tenant:

Contexto keyPrefijo de claseCarpeta PHPCarpeta VueArchivo de rutasNombre de ruta ejemploArchivos generados`central``Central``Central/``Pages/Central/``routes/web.php``central.users.index`24`shared``Shared``Shared/``Pages/Shared/``web.php` + `tenant.php``central.shared.invoices.index`16`tenant_shared``TenantShared``Tenant/Shared/``Pages/Tenant/Shared/``routes/tenant.php``roles.index` (sin prefijo)17`tenant` (ej: INNODITE)`TenantINNODITE``Tenant/INNODITE/``Pages/Tenant/INNODITE/``routes/tenant.php``innodite.products.index`20> **Descripción rápida de cada contexto:**
>
> - `central` → Panel administrativo global. Rutas en `web.php`. Prefijo `Central`.
> - `shared` → Código híbrido accesible tanto desde el panel central como desde el panel tenant. Inyecta rutas en DOS archivos simultáneamente.
> - `tenant_shared` → Estándar para todos los tenants. Sin prefijo de URL ni de nombre de ruta.
> - `tenant` → Tenants específicos del proyecto (INNODITE, ACME, etc.). Un array en `contexts.json`, cada entrada genera su propio espacio aislado.

---

🖥️ Arquitectura Frontend
------------------------

[](#️-arquitectura-frontend)

> **Regla fundamental — No negociable en este paquete.**

ResponsabilidadTecnologíaNavegación entre páginasInertia.js (`router.visit()`, `router.get()`)Carga y mutación de datosaxios (`GET`, `POST`, `PUT`, `DELETE`)Contexto activo y permisosProps de Inertia — compartidos por `InnoditeContextBridge`Los controladores utilizan el trait `RendersInertiaModule` y el método `renderModule()` para devolver la vista Inertia correcta según el contexto. **Nunca** pasan datos de negocio por props de Inertia.

Las vistas Vue son *shells* que se autocargan al montarse vía axios. Inertia nunca transporta datos de negocio; solo gestiona la navegación SPA.

```
// Controlador — uso de renderModule()
class CentralUserController extends Controller
{
    use RendersInertiaModule;

    public function index(): JsonResponse
    {
        $users = $this->service->paginate();
        return response()->json($users);
    }

    public function create(): \Inertia\Response
    {
        return $this->renderModule('CentralUserCreate');
        // Retorna la vista Inertia — sin datos de negocio
    }
}
```

```
// Vista Vue — carga sus propios datos al montarse
onMounted(async () => {
    const { data } = await axios.get(route(contextRoute('users.index')))
    items.value = data.data
})
```

---

🛠️ Guía de comandos
-------------------

[](#️-guía-de-comandos)

### `innodite:make-module` — Generador principal

[](#innoditemake-module--generador-principal)

Genera backend completo + vistas Vue en un solo comando.

```
# Módulo completo (backend + vistas + rutas inyectadas)
php artisan innodite:make-module User --context=central

# Selección interactiva de contexto
php artisan innodite:make-module User

# Tenant específico (por name, class_prefix o slug)
php artisan innodite:make-module Product --context=innodite

# Contexto shared (rutas en web.php Y tenant.php simultáneamente)
php artisan innodite:make-module Invoice --context=shared

# Sin inyección de rutas en el proyecto
php artisan innodite:make-module Report --context=central --no-routes

# Componentes individuales en módulo existente
php artisan innodite:make-module User --context=central -S -R   # Service + Repository
php artisan innodite:make-module User --context=central -C      # Controller + rutas
php artisan innodite:make-module User --context=central -G      # Migration
php artisan innodite:make-module User --context=central -M -Q   # Model + Request

# Desde JSON de configuración dinámica
php artisan innodite:make-module User --json
```

**Flags de componentes:**

FlagComponente generado`-M` / `--model`Modelo Eloquent con `$table` definida`-C` / `--controller`Controlador con `RendersInertiaModule` + inyección de rutas CRUD`-S` / `--service`Servicio + Interface en `Services/Contracts/``-R` / `--repository`Repositorio + Interface en `Repositories/Contracts/``-G` / `--migration`Migración anónima contextualizada`-Q` / `--request`Form Request validado (Store y Update para Central/Tenant, uno para Shared/TenantShared)**Validaciones de seguridad:**

- Nombres no PascalCase son rechazados
- Palabras reservadas de PHP y Laravel bloqueadas: `class`, `model`, `auth`, `route`, etc.
- Módulos duplicados bloqueados con opción de añadir componentes
- En caso de error, se ofrece **rollback** para eliminar archivos generados

---

### `innodite:add-entity` — Agregar entidad a módulo existente

[](#innoditeadd-entity--agregar-entidad-a-módulo-existente)

Agrega una nueva entidad a un módulo **ya existente**, generando sus componentes dentro de la subcarpeta de entidad correspondiente. Diseñado para módulos multi-entidad como `UserManagement` (con `User`, `Role`, `Permission`, `Module`).

```
# Agregar entidad Role al módulo UserManagement en contexto central
php artisan innodite:add-entity UserManagement Role --context=central

# Solo componentes específicos
php artisan innodite:add-entity UserManagement Permission --context=central -M -C -S -R -G -Q

# Sin inyectar rutas
php artisan innodite:add-entity UserManagement Module --context=central --no-routes

# Para un tenant específico
php artisan innodite:add-entity UserManagement Role --context=acme
```

**Firma:**

```
innodite:add-entity {module} {entity} {--context=} [-M] [-C] [-S] [-R] [-G] [-Q] [--no-routes]

```

ArgumentoDescripción`module`Nombre del módulo existente (ej: `UserManagement`)`entity`Nombre de la entidad nueva (ej: `Role`, `Permission`)`--context=`ID del contexto destino (ej: `central`, `acme`)`-M` a `-Q`Mismos flags que `make-module` (sin flags = genera todos los componentes)`--no-routes`Omite la inyección de rutas**Ejemplo de archivos generados** — `add-entity UserManagement Role --context=central`:

```
Modules/UserManagement/
├── Models/Central/Role/CentralRole.php
├── Http/Controllers/Central/Role/CentralRoleController.php
├── Http/Requests/Central/Role/CentralRoleStoreRequest.php
├── Http/Requests/Central/Role/CentralRoleUpdateRequest.php
├── Services/Central/Role/CentralRoleService.php
├── Services/Contracts/Central/Role/CentralRoleServiceInterface.php
├── Repositories/Central/Role/CentralRoleRepository.php
├── Repositories/Contracts/Central/Role/CentralRoleRepositoryInterface.php
└── Database/Migrations/Central/Role/..._create_roles_table.php

```

**Diferencia con `make-module`:**

`make-module``add-entity`Crea módulo nuevo✅❌Agrega a módulo existente❌✅Valida que el módulo exista primero—✅Sin flags = genera todos los componentes✅✅Naming convention intacta✅✅---

### `innodite:module-setup` — Configuración inicial

[](#innoditemodule-setup--configuración-inicial)

```
php artisan innodite:module-setup
```

Crea la estructura de configuración del paquete en la raíz del proyecto. Debe ejecutarse una sola vez al inicializar un nuevo proyecto que use este paquete.

---

### `innodite:module-check` — Diagnóstico de entorno

[](#innoditemodule-check--diagnóstico-de-entorno)

```
php artisan innodite:module-check
```

Verifica el entorno del proyecto e informa sobre:

1. `contexts.json` — validez, estructura y claves requeridas
2. Permisos de escritura en `Modules/`, `routes/`, `storage/logs/`
3. Colisiones de nombres entre módulos y ServiceProviders
4. Últimas 5 entradas del log de auditoría

---

### `innodite:check-env` — Contrato de Datos Frontend-Backend

[](#innoditecheck-env--contrato-de-datos-frontend-backend)

```
php artisan innodite:check-env
```

Verifica el bridge Inertia y, si algo falta, imprime el **bloque de código exacto** a copiar:

1. Modelo User — `HasRoles` (Spatie) o `InnoditeUserPermissions`
2. `HandleInertiaRequests` — `auth.permissions` compartido
3. `InnoditeContextBridge` — registrado en el stack web

---

### `innodite:publish-frontend` — Composables Vue 3

[](#innoditepublish-frontend--composables-vue-3)

```
php artisan innodite:publish-frontend
php artisan innodite:publish-frontend --force  # sobreescribir
```

Publica en `resources/js/Composables/`:

- `useModuleContext.js`
- `usePermissions.js`

---

### `innodite:migrate-plan` — Orquestador de Migraciones por Manifiesto

[](#innoditemigrate-plan--orquestador-de-migraciones-por-manifiesto)

Ejecuta migraciones en el orden exacto definido en un manifiesto JSON. Es ideal cuando hay dependencias entre módulos y contextos. Antes de ejecutar, valida la conexión objetivo y verifica que la base de datos exista para evitar procesos parciales o lanzados contra una BD incorrecta.

```
# Usar manifiesto por defecto (module-maker-config/migrations/central_order.json)
php artisan innodite:migrate-plan

# Usar manifiesto específico
php artisan innodite:migrate-plan --manifest=tenant_innodite_order.json

# Simular sin tocar BD
php artisan innodite:migrate-plan --manifest=tenant_innodite_order.json --dry-run

# Ejecutar también seeders después de migraciones
php artisan innodite:migrate-plan --manifest=tenant_innodite_order.json --seed
```

**Formato de coordenadas soportado:**

- Migraciones: `Modulo:Contexto/Archivo.php`
- Seeders: `Modulo:Contexto/ClaseSeeder`

**Ejemplo real de manifiesto (`module-maker-config/migrations/tenant_innodite_order.json`):**

```
{
    "migrations": [
        "User:Shared/2026_01_01_000001_create_users_table.php",
        "Roles:Tenant/Shared/2026_02_01_000001_create_tenant_roles_table.php",
        "Custom:Tenant/INNODITE/2026_03_01_000001_innodite_extra_table.php"
    ],
    "seeders": [
        "User:Shared/SharedUserSeeder",
        "Roles:Tenant/Shared/TenantSharedRoleSeeder",
        "Custom:Tenant/INNODITE/TenantINNODITECustomSeeder"
    ]
}
```

**Cómo resuelve rutas internas:**

- `User:Shared/2026_...php` → `Modules/User/Database/Migrations/Shared/2026_...php`
- `Roles:Tenant/Shared/TenantSharedRoleSeeder` → `Modules\Roles\Database\Seeders\Tenant\Shared\TenantSharedRoleSeeder`

**Qué valida el comando:**

- Que el manifiesto exista y sea JSON válido
- Que `migrations` y `seeders` sean arrays
- Que cada coordenada de migración apunte a un archivo real
- Que el formato de coordenada sea correcto

**Mensajes de error claros:**

Si una coordenada no existe, el comando responde con la ruta esperada para corregirla rápidamente. Si la base de datos objetivo no existe, corta el proceso antes de ejecutar migraciones o seeders.

---

### `innodite:migrate-one` — Ejecutar una Migración Específica

[](#innoditemigrate-one--ejecutar-una-migración-específica)

Permite ejecutar una coordenada de migración puntual sin correr el manifiesto completo. Está pensado para casos quirúrgicos donde necesitas lanzar una sola migración y mantener sincronizado el manifiesto correspondiente.

```
# Ejecutar una migración específica
php artisan innodite:migrate-one "Products:Tenant/Alpha/2026_01_01_000001_create_products_table.php"

# Forzar un manifiesto concreto
php artisan innodite:migrate-one "Forms:Shared/2026_01_01_000001_create_forms_table.php" --manifest=central_order.json

# Simular sin escribir ni ejecutar
php artisan innodite:migrate-one "Forms:Shared/2026_01_01_000001_create_forms_table.php" --dry-run

# Omitir confirmaciones interactivas
php artisan innodite:migrate-one "Products:Tenant/Alpha/2026_01_01_000001_create_products_table.php" --yes
```

**Qué hace internamente:**

1. Resuelve la ruta exacta del archivo de migración desde la coordenada.
2. Detecta automáticamente el manifiesto objetivo según el contexto.
3. Si la coordenada aplica a múltiples manifiestos, muestra los destinos y pide confirmación.
4. Muestra antes de ejecutar:
    - Tipo: migración
    - Coordenada
    - Conexión
    - Base de datos
    - Manifiesto destino
    - Ruta real del archivo
5. Si la coordenada no está registrada en el manifiesto, la agrega primero.
6. Ejecuta solo la migración indicada.

**Reglas de resolución:**

- `Central` =&gt; `central_order.json`
- `Shared` =&gt; puede aplicar a `central_order.json` y a los manifiestos tenant
- `Tenant/Shared` =&gt; aplica a todos los manifiestos tenant
- `Tenant/X` =&gt; aplica al manifiesto `tenant_x_order.json` correspondiente

**Importante:**

- Requiere confirmación interactiva antes de ejecutar, salvo que uses `--yes`.
- En `--dry-run` no modifica el manifiesto ni ejecuta nada.
- Si la base de datos objetivo no existe, falla antes de iniciar el proceso.

---

### `innodite:seed-one` — Ejecutar un Seeder Específico

[](#innoditeseed-one--ejecutar-un-seeder-específico)

Permite ejecutar un seeder puntual sin correr el manifiesto completo. Está pensado para casos quirúrgicos donde necesitas lanzar un solo seeder y mantener sincronizado el manifiesto correspondiente.

```
# Ejecutar un seeder específico
php artisan innodite:seed-one "UserManagement:Tenant/Shared/TenantSharedPermissionSeeder"

# Forzar un manifiesto concreto
php artisan innodite:seed-one "Forms:Shared/SharedFormsSeeder" --manifest=central_order.json

# Simular sin escribir ni ejecutar
php artisan innodite:seed-one "Forms:Shared/SharedFormsSeeder" --dry-run

# Omitir confirmaciones interactivas
php artisan innodite:seed-one "UserManagement:Tenant/Shared/TenantSharedPermissionSeeder" --yes
```

**Qué hace internamente:**

1. Resuelve el FQCN (clase completa) del seeder desde la coordenada.
2. Detecta automáticamente el manifiesto objetivo según el contexto.
3. Si la coordenada aplica a múltiples manifiestos, muestra los destinos y pide confirmación.
4. Muestra antes de ejecutar:
    - Tipo: seeder
    - Coordenada
    - Conexión
    - Base de datos
    - Manifiesto destino
    - Clase real que va a ejecutar
5. Si la coordenada no está registrada en el manifiesto, la agrega primero.
6. Ejecuta solo el seeder indicado.

**Reglas de resolución:**

- `Central` =&gt; `central_order.json`
- `Shared` =&gt; puede aplicar a `central_order.json` y a los manifiestos tenant
- `Tenant/Shared` =&gt; aplica a todos los manifiestos tenant
- `Tenant/X` =&gt; aplica al manifiesto `tenant_x_order.json` correspondiente

**Importante:**

- Requiere confirmación interactiva antes de ejecutar, salvo que uses `--yes`.
- En `--dry-run` no modifica el manifiesto ni ejecuta nada.
- Si la base de datos objetivo no existe, falla antes de iniciar el proceso.

---

### `innodite:migration-sync` — Sincronización Automática de Manifiestos

[](#innoditemigration-sync--sincronización-automática-de-manifiestos)

Escanea los módulos y agrega al manifiesto las migraciones y seeders que aún no están registradas.

```
# Sincronizar automaticamente por contextos (central + tenants detectados)
php artisan innodite:migration-sync

# Sincronizar un manifiesto concreto
php artisan innodite:migration-sync --manifest=tenant_innodite_order.json

# Sincronizacion automatica sin prompt de confirmacion
php artisan innodite:migration-sync --yes

# Ver faltantes sin escribir cambios
php artisan innodite:migration-sync --manifest=tenant_innodite_order.json --dry-run
```

**Comportamiento de sync:**

1. Si no envías `--manifest`, lee `module-maker-config/contexts.json` y propone:
    - `central_order.json`
    - `tenant_{permission_prefix}_order.json` por cada tenant configurado.
2. Pide confirmación en consola antes de generar/sincronizar múltiples manifiestos (omite prompt con `--yes`).
3. Crea `module-maker-config/migrations/` si no existe.
4. Crea cada manifiesto faltante (estructura vacía).
5. Escanea:
    - `Modules/*/Database/Migrations/**`
    - `Modules/*/Database/Seeders/**`
6. Convierte hallazgos a coordenadas.
7. Filtra por alcance de manifiesto:
    - `central_order.json` =&gt; contextos `Central` y `Shared`.
    - `tenant_*.json` =&gt; `Shared` + `Tenant/Shared` + contexto `Tenant/{X}` del tenant objetivo.
8. Hace append solo de faltantes (sin duplicar).

**Importante:**

- Solo sincroniza archivos en subcarpetas de contexto (`Shared`, `Central`, `Tenant/...`).
- Esto mantiene consistencia con el modelo contextual del paquete.

**Cuándo usarlo en la práctica:**

- Después de generar nuevos módulos/entidades y querer actualizar manifiestos automáticamente.
- Antes de un deploy, para verificar que no quedaron migraciones fuera del plan.
- En CI/CD para detectar drift entre código y manifiesto.

---

### `innodite:test-module` — Ejecutar Tests con Coverage

[](#innoditetest-module--ejecutar-tests-con-coverage)

```
# 1) Sincronizar configuración por contexto (crea Tests/test-config.json)
php artisan innodite:test-sync User

# 2) Ejecutar tests de un módulo (modo default sin contexto)
php artisan innodite:test-module User

# 3) Ejecutar un contexto específico definido en test-config.json
php artisan innodite:test-module User --context=central

# 4) Ejecutar todos los contextos habilitados del módulo
php artisan innodite:test-module User --all-contexts

# 5) Coverage por módulo/contexto
php artisan innodite:test-module User --context=central --coverage --format=html --format=clover
```

**Características:**

- ✅ Ejecuta PHPUnit en uno o todos los módulos
- ✅ Usa configuración contextual en `Modules/{Modulo}/Tests/test-config.json`
- ✅ Permite correr un contexto (`--context`) o todos los contextos habilitados (`--all-contexts`)
- ✅ Escanea recursivamente toda la carpeta `Tests/` sin asumir estructura fija
- ✅ Crea/usa archivo de configuración PHPUnit editable en `Modules/{Modulo}/Tests/phpunit-{contexto}.xml`
- ✅ Puede ejecutar un `seeder` previo por contexto antes de PHPUnit
- ✅ Genera reportes de coverage en múltiples formatos:
    - **HTML** → `docs/test-reports/{Module}/{contexto}/html/index.html` (navegable)
    - **Text** → Salida en consola con porcentajes
        - **Clover XML** → `docs/test-reports/{Module}/{contexto}/clover.xml` (CI/CD)
- ✅ Valida que Xdebug o PCOV estén activos para coverage
- ✅ Muestra tabla resumen con resultados y porcentaje de cobertura
- ✅ Soporta flag `--filter` de PHPUnit para tests específicos
- ✅ Detección automática de módulos sin tests (warning + continuar)

### `innodite:test-sync` — Sincronizar `Tests/test-config.json`

[](#innoditetest-sync--sincronizar-teststest-configjson)

Genera o actualiza el archivo `test-config.json` dentro de la carpeta `Tests/` de cada módulo, leyendo los contextos desde `module-maker-config/contexts.json`.

Para testing, el sync solo genera contextos válidos de ejecución:

- `central`
- tenants específicos (`tenant_alpha`, `tenant_beta`, etc.)

No genera `shared` ni `tenant_shared`, porque esos contextos no representan una base de datos de prueba autónoma.

```
# Sincronizar un módulo
php artisan innodite:test-sync User

# Sincronizar todos los módulos
php artisan innodite:test-sync --all
```

**Reglas del sync:**

- ✅ Crea `Modules/{Modulo}/Tests/test-config.json` si no existe
- ✅ Agrega contextos faltantes sin duplicar
- ✅ Conserva overrides manuales de `db_connection`, `db_database`, `seeder`, `enabled` y `env`
- ✅ No asume ninguna base de datos por defecto: tú defines `db_connection` y `db_database`

Ejemplo de `Modules/User/Tests/test-config.json`:

```
{
    "_readme": "Configuración de tests por contexto. Generado por innodite:test-sync.",
    "contexts": {
        "central": {
            "db_connection": "mysql",
            "db_database": "innodite_test",
            "enabled": true,
            "seeder": null,
            "env": {}
        },
        "tenant_alpha": {
            "db_connection": "tenant",
            "db_database": "tenant_alpha_test",
            "enabled": true,
            "seeder": "Modules\\UserManagement\\Database\\Seeders\\Tenant\\TenantAlphaSeeder",
            "env": {
                "CACHE_DRIVER": "array"
            }
        }
    }
}
```

**Requisitos para Coverage:**

```
# Opción 1: Xdebug (desarrollo)
pecl install xdebug
# Añadir a php.ini: zend_extension=xdebug.so

# Opción 2: PCOV (más rápido, CI/CD)
pecl install pcov
# Añadir a php.ini: extension=pcov.so
```

**Ejemplo de Salida:**

```
🧪 Innodite Module Maker - Test Runner

✅ PHPUnit encontrado
✅ Xdebug activo - Coverage disponible

📦 Módulos a testear: User, Product, Invoice

🔍 Ejecutando tests del módulo: User
  📄 Archivos de test encontrados: 12
  ✓ Tests passed (15 tests, 45 assertions)

═══════════════════════════════════════════════════════
📊 RESUMEN DE EJECUCIÓN
═══════════════════════════════════════════════════════

┌─────────┬─────────┬──────────┐
│ Módulo  │ Estado  │ Coverage │
├─────────┼─────────┼──────────┤
│ User    │ ✓ PASSED│ 87.5%    │
│ Product │ ✓ PASSED│ 92.3%    │
│ Invoice │ ✗ FAILED│ 65.2%    │
└─────────┴─────────┴──────────┘

Total: 3 | Passed: 2 | Failed: 1 | Skipped: 0

📁 Reportes de coverage guardados en:
   docs/test-reports/
   • User: docs/test-reports/User/html/index.html
   • Product: docs/test-reports/Product/html/index.html

```

---

📁 Archivos generados por contexto
---------------------------------

[](#-archivos-generados-por-contexto)

Esta sección muestra la lista exacta de archivos que el paquete genera para el módulo `User` en cada uno de los 4 contextos.

---

### Contexto `central` — 24 archivos

[](#contexto-central--24-archivos)

```
Modules/User/
├── Http/Controllers/Central/User/CentralUserController.php
├── Http/Requests/Central/User/CentralUserStoreRequest.php
├── Http/Requests/Central/User/CentralUserUpdateRequest.php
├── Services/Central/User/CentralUserService.php
├── Services/Contracts/Central/User/CentralUserServiceInterface.php
├── Repositories/Central/User/CentralUserRepository.php
├── Repositories/Contracts/Central/User/CentralUserRepositoryInterface.php
├── Models/Central/User/CentralUser.php
├── Database/Migrations/Central/User/XXXX_create_users_table.php
├── Database/Seeders/Central/User/CentralUserSeeder.php
├── Database/Factories/Central/User/CentralUserFactory.php
├── Tests/Feature/Central/CentralUserTest.php
├── Tests/Unit/Central/CentralUserServiceTest.php
├── Tests/Support/Central/CentralUserSupport.php
├── Resources/js/Pages/Central/CentralUserIndex.vue
├── Resources/js/Pages/Central/CentralUserCreate.vue
├── Resources/js/Pages/Central/CentralUserEdit.vue
├── Resources/js/Pages/Central/CentralUserShow.vue
├── Jobs/Central/CentralUserExportJob.php
├── Notifications/Central/CentralUserWelcomeNotification.php
├── Console/Commands/Central/CentralUserCleanupCommand.php
├── Exceptions/Central/CentralUserNotFoundException.php
├── Providers/UserServiceProvider.php
└── Routes/web.php

```

> **v3.5.x** — Los componentes principales (Model, Controller, Requests, Service, Repository, Migration) se generan dentro de una subcarpeta con el nombre de la entidad: `{Tipo}/{Contexto}/{Entidad}/`. Las vistas Vue, Tests, Jobs, Notifications y Commands mantienen su estructura anterior (sin subcarpeta de entidad).

---

### Contexto `shared` — 16 archivos

[](#contexto-shared--16-archivos)

```
Modules/User/
├── Http/Controllers/Shared/User/SharedUserController.php
├── Http/Requests/Shared/User/SharedUserRequest.php
├── Services/Shared/User/SharedUserService.php
├── Services/Contracts/Shared/User/SharedUserServiceInterface.php
├── Repositories/Shared/User/SharedUserRepository.php
├── Repositories/Contracts/Shared/User/SharedUserRepositoryInterface.php
├── Models/Shared/User/SharedUser.php
├── Database/Migrations/Shared/User/XXXX_create_users_table.php
├── Database/Seeders/Shared/User/SharedUserSeeder.php
├── Database/Factories/Shared/User/SharedUserFactory.php
├── Tests/Feature/Shared/SharedUserTest.php
├── Tests/Unit/Shared/SharedUserServiceTest.php
├── Resources/js/Pages/Shared/SharedUserIndex.vue
├── Resources/js/Pages/Shared/SharedUserCreate.vue
├── Resources/js/Pages/Shared/SharedUserEdit.vue
└── Resources/js/Pages/Shared/SharedUserShow.vue

```

---

### Contexto `tenant_shared` — 17 archivos

[](#contexto-tenant_shared--17-archivos)

```
Modules/User/
├── Http/Controllers/Tenant/Shared/User/TenantSharedUserController.php
├── Http/Requests/Tenant/Shared/User/TenantSharedUserRequest.php
├── Services/Tenant/Shared/User/TenantSharedUserService.php
├── Services/Contracts/Tenant/Shared/User/TenantSharedUserServiceInterface.php
├── Repositories/Tenant/Shared/User/TenantSharedUserRepository.php
├── Repositories/Contracts/Tenant/Shared/User/TenantSharedUserRepositoryInterface.php
├── Models/Tenant/Shared/User/TenantSharedUser.php
├── Database/Migrations/Tenant/Shared/User/XXXX_create_users_table.php
├── Database/Seeders/Tenant/Shared/User/TenantSharedUserSeeder.php
├── Database/Factories/Tenant/Shared/User/TenantSharedUserFactory.php
├── Tests/Feature/Tenant/Shared/TenantSharedUserTest.php
├── Tests/Unit/Tenant/Shared/TenantSharedUserServiceTest.php
├── Resources/js/Pages/Tenant/Shared/TenantSharedUserIndex.vue
├── Resources/js/Pages/Tenant/Shared/TenantSharedUserCreate.vue
├── Resources/js/Pages/Tenant/Shared/TenantSharedUserEdit.vue
├── Resources/js/Pages/Tenant/Shared/TenantSharedUserShow.vue
└── Jobs/Tenant/Shared/TenantSharedUserReportJob.php

```

---

### Contexto `tenant` (ej: INNODITE) — 20 archivos

[](#contexto-tenant-ej-innodite--20-archivos)

```
Modules/User/
├── Http/Controllers/Tenant/INNODITE/User/TenantINNODITEUserController.php
├── Http/Requests/Tenant/INNODITE/User/TenantINNODITEUserStoreRequest.php
├── Http/Requests/Tenant/INNODITE/User/TenantINNODITEUserUpdateRequest.php
├── Services/Tenant/INNODITE/User/TenantINNODITEUserService.php
├── Services/Contracts/Tenant/INNODITE/User/TenantINNODITEUserServiceInterface.php
├── Repositories/Tenant/INNODITE/User/TenantINNODITEUserRepository.php
├── Repositories/Contracts/Tenant/INNODITE/User/TenantINNODITEUserRepositoryInterface.php
├── Models/Tenant/INNODITE/User/TenantINNODITEUser.php
├── Database/Migrations/Tenant/INNODITE/User/XXXX_create_users_table.php
├── Database/Seeders/Tenant/INNODITE/User/TenantINNODITEUserSeeder.php
├── Database/Factories/Tenant/INNODITE/User/TenantINNODITEUserFactory.php
├── Tests/Feature/Tenant/INNODITE/TenantINNODITEUserTest.php
├── Tests/Unit/Tenant/INNODITE/TenantINNODITEUserServiceTest.php
├── Resources/js/Pages/Tenant/INNODITE/TenantINNODITEUserIndex.vue
├── Resources/js/Pages/Tenant/INNODITE/TenantINNODITEUserCreate.vue
├── Resources/js/Pages/Tenant/INNODITE/TenantINNODITEUserEdit.vue
├── Resources/js/Pages/Tenant/INNODITE/TenantINNODITEUserShow.vue
├── Jobs/Tenant/INNODITE/TenantINNODITEUserReportJob.php
├── Notifications/Tenant/INNODITE/TenantINNODITEUserCustomAlert.php
└── Console/Commands/Tenant/INNODITE/TenantINNODITEUserImportCommand.php

```

---

🔄 Flujo completo por contexto
-----------------------------

[](#-flujo-completo-por-contexto)

Esta sección documenta el flujo de generación completo para cada contexto: qué archivos crea, dónde los ubica y cómo inyecta las rutas.

---

### Contexto `central`

[](#contexto-central)

```
php artisan innodite:make-module User --context=central
```

#### Ruta inyectada en `routes/web.php`

[](#ruta-inyectada-en-routeswebphp)

```
// Bloque generado para: User (Contexto: App Central)
Route::prefix('central')->name('central.')->middleware(['web','auth'])->group(function () {
    Route::prefix('users')->name('users.')->group(function () {
        Route::get('/',          [CentralUserController::class, 'index'])->name('index');
        Route::get('/create',    [CentralUserController::class, 'create'])->name('create');
        Route::post('/',         [CentralUserController::class, 'store'])->name('store');
        Route::get('/{id}',      [CentralUserController::class, 'show'])->name('show');
        Route::get('/{id}/edit', [CentralUserController::class, 'edit'])->name('edit');
        Route::put('/{id}',      [CentralUserController::class, 'update'])->name('update');
        Route::delete('/{id}',   [CentralUserController::class, 'destroy'])->name('destroy');
    });
});
// {{CENTRAL_ROUTES_END}}
```

#### Resolución de `contextRoute()`

[](#resolución-de-contextroute)

```
contextRoute('users.index')
// Resuelve: 'central.users.index'
```

---

### Contexto `shared`

[](#contexto-shared)

```
php artisan innodite:make-module Invoice --context=shared
```

#### Dualidad de rutas — inyección simultánea en DOS archivos

[](#dualidad-de-rutas--inyección-simultánea-en-dos-archivos)

El contexto `shared` es único: sus rutas son accesibles tanto desde el panel central como desde el panel tenant. El generador inyecta rutas en **dos archivos simultáneamente**.

**En `routes/web.php`** (acceso desde el panel central):

```
// Bloque generado para: Invoice (Contexto: Shared — panel central)
Route::prefix('central/shared')->name('central.shared.')->middleware(['web','auth'])->group(function () {
    Route::prefix('invoices')->name('invoices.')->group(function () {
        Route::get('/',          [SharedInvoiceController::class, 'index'])->name('index');
        Route::get('/create',    [SharedInvoiceController::class, 'create'])->name('create');
        Route::post('/',         [SharedInvoiceController::class, 'store'])->name('store');
        Route::get('/{id}',      [SharedInvoiceController::class, 'show'])->name('show');
        Route::get('/{id}/edit', [SharedInvoiceController::class, 'edit'])->name('edit');
        Route::put('/{id}',      [SharedInvoiceController::class, 'update'])->name('update');
        Route::delete('/{id}',   [SharedInvoiceController::class, 'destroy'])->name('destroy');
    });
});
// {{CENTRAL_ROUTES_END}}
```

**En `routes/tenant.php`** (acceso desde el panel tenant):

```
// Bloque generado para: Invoice (Contexto: Shared — panel tenant)
Route::prefix('tenant/shared')->name('tenant.shared.')->middleware(['web','auth'])->group(function () {
    Route::prefix('invoices')->name('invoices.')->group(function () {
        Route::get('/',          [SharedInvoiceController::class, 'index'])->name('index');
        Route::get('/create',    [SharedInvoiceController::class, 'create'])->name('create');
        Route::post('/',         [SharedInvoiceController::class, 'store'])->name('store');
        Route::get('/{id}',      [SharedInvoiceController::class, 'show'])->name('show');
        Route::get('/{id}/edit', [SharedInvoiceController::class, 'edit'])->name('edit');
        Route::put('/{id}',      [SharedInvoiceController::class, 'update'])->name('update');
        Route::delete('/{id}',   [SharedInvoiceController::class, 'destroy'])->name('destroy');
    });
});
// {{TENANT_SHARED_ROUTES_END}}
```

#### Resolución de `contextRoute()` en `shared`

[](#resolución-de-contextroute-en-shared)

El mismo componente Vue resuelve diferente según el panel activo, gracias a `auth.context.route_prefix` inyectada por `InnoditeContextBridge`:

```
// Desde el panel central (route_prefix = 'central.shared')
contextRoute('invoices.index')
// Resuelve: 'central.shared.invoices.index'

// Desde el panel tenant (route_prefix = 'tenant.shared')
contextRoute('invoices.index')
// Resuelve: 'tenant.shared.invoices.index'
```

Las vistas Vue no cambian — el composable adapta la ruta automáticamente según el contexto activo en sesión.

---

### Contexto `tenant_shared`

[](#contexto-tenant_shared)

```
php artisan innodite:make-module Role --context=tenant_shared
```

#### Ruta inyectada en `routes/tenant.php`

[](#ruta-inyectada-en-routestenantphp)

El contexto `tenant_shared` tiene `route_prefix: null` — las rutas se definen sin prefijo URL para que cada tenant acceda directamente bajo su propio dominio.

```
// Bloque generado para: Role (Contexto: Tenant Shared)
Route::middleware(['web','auth'])->group(function () {
    Route::prefix('roles')->name('roles.')->group(function () {
        Route::get('/',          [TenantSharedRoleController::class, 'index'])->name('index');
        Route::get('/create',    [TenantSharedRoleController::class, 'create'])->name('create');
        Route::post('/',         [TenantSharedRoleController::class, 'store'])->name('store');
        Route::get('/{id}',      [TenantSharedRoleController::class, 'show'])->name('show');
        Route::get('/{id}/edit', [TenantSharedRoleController::class, 'edit'])->name('edit');
        Route::put('/{id}',      [TenantSharedRoleController::class, 'update'])->name('update');
        Route::delete('/{id}',   [TenantSharedRoleController::class, 'destroy'])->name('destroy');
    });
});
// {{TENANT_SHARED_ROUTES_END}}
```

> **Nota:** Sin `route_prefix`, el nombre de ruta tampoco lleva prefijo de contexto. `contextRoute('roles.index')` devuelve simplemente `'roles.index'`.

---

### Contexto `tenant` (tenant específico — ej: INNODITE)

[](#contexto-tenant-tenant-específico--ej-innodite)

```
php artisan innodite:make-module Product --context=innodite
```

El paquete resuelve `innodite` buscando en el array `tenant` de `contexts.json` por `name`, `class_prefix` o slug derivado del nombre.

#### Ruta inyectada en `routes/tenant.php`

[](#ruta-inyectada-en-routestenantphp-1)

```
// Bloque generado para: Product (Contexto: INNODITE)
Route::prefix('innodite')->name('innodite.')->middleware(['web','auth','tenant-auth'])->group(function () {
    Route::prefix('products')->name('products.')->group(function () {
        Route::get('/',          [TenantINNODITEProductController::class, 'index'])->name('index');
        Route::get('/create',    [TenantINNODITEProductController::class, 'create'])->name('create');
        Route::post('/',         [TenantINNODITEProductController::class, 'store'])->name('store');
        Route::get('/{id}',      [TenantINNODITEProductController::class, 'show'])->name('show');
        Route::get('/{id}/edit', [TenantINNODITEProductController::class, 'edit'])->name('edit');
        Route::put('/{id}',      [TenantINNODITEProductController::class, 'update'])->name('update');
        Route::delete('/{id}',   [TenantINNODITEProductController::class, 'destroy'])->name('destroy');
    });
});
// {{TENANT_INNODITE_ROUTES_END}}
```

#### Resolución de `contextRoute()`

[](#resolución-de-contextroute-1)

```
contextRoute('products.index')
// Resuelve: 'innodite.products.index'
```

---

🧩 Composables Vue 3
-------------------

[](#-composables-vue-3)

Los composables se publican con `php artisan innodite:publish-frontend` en `resources/js/Composables/`.

### `useModuleContext` — Detección automática de contexto

[](#usemodulecontext--detección-automática-de-contexto)

Lee `auth.context.route_prefix` desde las props de Inertia compartidas por `InnoditeContextBridge` y antepone automáticamente el prefijo correcto a cualquier clave de ruta.

```
import { useModuleContext } from '@/Composables/useModuleContext'

const { contextRoute, routePrefix, permissionPrefix } = useModuleContext()

route(contextRoute('users.index'))
// Central              → 'central.users.index'
// Shared (web)         → 'central.shared.users.index'
// Shared (tenant)      → 'tenant.shared.users.index'
// TenantShared         → 'users.index'  (sin prefijo)
// Tenant INNODITE      → 'innodite.users.index'
```

El mismo componente Vue funciona en cualquier contexto sin cambios — el composable resuelve la ruta correcta según la sesión activa.

---

### `usePermissions` — Verificación de permisos del usuario

[](#usepermissions--verificación-de-permisos-del-usuario)

Lee `auth.permissions` desde las props de Inertia y permite verificar permisos de forma declarativa en las plantillas Vue.

```
import { usePermissions } from '@/Composables/usePermissions'

const { can, canAny, canAll } = usePermissions()

can('users.create')                          // true/false
canAny(['users.edit', 'users.create'])       // true si tiene al menos uno
canAll(['users.view', 'users.edit'])         // true si tiene todos
```

**Estrategia dual:** verifica `{prefix}.{perm}` y `{perm}` plano simultáneamente. El mismo componente funciona en cualquier contexto sin cambios.

```

    Nuevo usuario

  Editar
  Eliminar

```

---

### Flujo de datos en las vistas Vue generadas

[](#flujo-de-datos-en-las-vistas-vue-generadas)

```
Montaje  → axios.get(route(contextRoute('users.index')))    ← carga datos
Guardar  → axios.post/put(route(...))                       ← muta datos
Navegar  → router.visit(route(contextRoute('users.xxx')))   ← Inertia solo navega
Permisos → can('users.edit')                                ← oculta/muestra UI

```

### Ejemplo — `CentralUserIndex.vue`

[](#ejemplo--centraluserindexvue)

```

import { ref, onMounted } from 'vue'
import { router } from '@inertiajs/vue3'
import { useModuleContext } from '@/Composables/useModuleContext'
import { usePermissions } from '@/Composables/usePermissions'

const { contextRoute } = useModuleContext()
const { can }          = usePermissions()

const items = ref([])
const meta  = ref({ current_page: 1, last_page: 1, total: 0 })

async function fetchItems(page = 1) {
    const { data } = await axios.get(route(contextRoute('users.index')), { params: { page } })
    items.value = data.data
    meta.value  = { current_page: data.current_page, last_page: data.last_page, total: data.total }
}

async function destroy(id) {
    if (!confirm('¿Eliminar?')) return
    await axios.delete(route(contextRoute('users.destroy'), { id }))
    fetchItems(meta.value.current_page)
}

onMounted(() => fetchItems())

```

### Ejemplo — `CentralUserCreate.vue`

[](#ejemplo--centralusercreatevue)

```
async function submit() {
    await axios.post(route(contextRoute('users.store')), form.value)
    router.visit(route(contextRoute('users.index')))  // navega con Inertia
}
```

- Errores de validación Laravel 422 mostrados campo a campo
- Botón deshabilitado durante el envío (previene doble submit)

### Ejemplo — `CentralUserEdit.vue`

[](#ejemplo--centralusereditvue)

```
onMounted(async () => {
    const { data } = await axios.get(route(contextRoute('users.show'), { id: props.id }))
    form.value = { ...data }  // rellena el formulario con datos existentes
})

async function submit() {
    await axios.put(route(contextRoute('users.update'), { id: props.id }), form.value)
    router.visit(route(contextRoute('users.index')))
}
```

- Recibe solo `id` como prop de Inertia (nunca el objeto completo)
- Carga el registro vía axios al montarse

---

🔧 Stubs contextuales
--------------------

[](#-stubs-contextuales)

El sistema de stubs de v3.1.0 organiza las plantillas en **4 carpetas independientes**, una por contexto. Esto permite personalizar la salida generada para cada contexto sin afectar los demás.

### Estructura de stubs

[](#estructura-de-stubs)

```
module-maker-config/
└── stubs/
    └── contextual/
        ├── Central/
        │   ├── controller.stub
        │   ├── service.stub
        │   ├── repository.stub
        │   ├── model.stub
        │   ├── request-store.stub
        │   ├── request-update.stub
        │   ├── vue-index.stub
        │   ├── vue-create.stub
        │   ├── vue-edit.stub
        │   └── vue-show.stub
        ├── Shared/
        │   ├── controller.stub
        │   ├── service.stub
        │   └── ...
        ├── TenantShared/
        │   ├── controller.stub
        │   ├── service.stub
        │   └── ...
        └── TenantName/
            ├── controller.stub
            ├── service.stub
            └── ...

```

### Publicar stubs para personalización

[](#publicar-stubs-para-personalización)

```
php artisan vendor:publish --tag=module-maker-stubs
```

Copia las 4 carpetas de stubs a `module-maker-config/stubs/contextual/` en tu proyecto. A partir de ese momento, el generador usará tus stubs en lugar de los del paquete.

### Variables disponibles en los stubs

[](#variables-disponibles-en-los-stubs)

VariableDescripciónEjemplo`{{MODULE}}`Nombre del módulo`User``{{CLASS_PREFIX}}`Prefijo de clase del contexto`Central``{{NAMESPACE}}`Namespace completo de la clase`Modules\User\Http\Controllers\Central``{{CLASS_NAME}}`Nombre completo de la clase`CentralUserController``{{MODEL_CLASS}}`Clase del modelo`CentralUser``{{SERVICE_INTERFACE}}`Interface del servicio`CentralUserServiceInterface``{{ROUTE_PREFIX}}`Prefijo de ruta del contexto`central``{{TABLE_NAME}}`Nombre de la tabla`central_users`---

🌉 Bridge Frontend-Backend
-------------------------

[](#-bridge-frontend-backend)

### Middleware `InnoditeContextBridge`

[](#middleware-innoditecontextbridge)

Intercepta cada request e inyecta vía `Inertia::share()`:

PropValor ejemplo`auth.context.route_prefix``central`, `innodite`, `central.shared``auth.context.permission_prefix``central`, `innodite`, `tenant``auth.permissions``['central.users.edit', 'users.view', ...]`**Cadena de resolución de permisos:**

1. Spatie Permission → `$user->getAllPermissions()->pluck('name')`
2. `InnoditeUserPermissions` → `$user->getInnoditePermissions()`
3. Fail-safe → `[]` + Warning en log

**Registrar en `bootstrap/app.php` (Laravel 11+):**

```
->withMiddleware(function (Middleware $middleware) {
    $middleware->appendToGroup('web', [
        \Innodite\LaravelModuleMaker\Middleware\InnoditeContextBridge::class,
    ]);
})
```

**Alias para rutas específicas:**

```
Route::middleware('innodite.bridge')->group(fn() => ...);
```

---

### Interfaz `InnoditeUserPermissions`

[](#interfaz-innoditeuserpermissions)

```
use Innodite\LaravelModuleMaker\Contracts\InnoditeUserPermissions;

class User extends Authenticatable implements InnoditeUserPermissions
{
    public function getInnoditePermissions(): array
    {
        return $this->permissions->pluck('name')->toArray();
    }
}
```

---

⚙️ Estructura de contextos (`contexts.json`)
--------------------------------------------

[](#️-estructura-de-contextos-contextsjson)

```
{
    "contexts": {
        "central": [{
            "name": "App Central",
            "class_prefix": "Central",
            "folder": "Central",
            "namespace_path": "Central",
            "route_file": "web.php",
            "route_prefix": "central",
            "route_name": "central.",
            "permission_prefix": "central",
            "route_middleware": ["web", "auth"]
        }],
        "shared": [{
            "name": "Shared",
            "class_prefix": "Shared",
            "folder": "Shared",
            "namespace_path": "Shared",
            "route_file": ["web.php", "tenant.php"],
            "web_route_prefix": "central.shared",
            "web_route_name": "central.shared.",
            "tenant_route_prefix": "tenant.shared",
            "tenant_route_name": "tenant.shared.",
            "route_middleware": []
        }],
        "tenant_shared": [{
            "name": "Tenant Shared",
            "class_prefix": "TenantShared",
            "folder": "Tenant/Shared",
            "namespace_path": "Tenant\\Shared",
            "route_file": "tenant.php",
            "route_prefix": null,
            "route_name": null,
            "permission_prefix": "tenant",
            "route_middleware": []
        }],
        "tenant": [
            {
                "name": "INNODITE",
                "class_prefix": "TenantINNODITE",
                "folder": "Tenant/INNODITE",
                "namespace_path": "Tenant\\INNODITE",
                "route_file": "tenant.php",
                "route_prefix": "innodite",
                "route_name": "innodite.",
                "permission_prefix": "innodite",
                "route_middleware": ["web", "auth", "tenant-auth"]
            },
            {
                "name": "ACME",
                "class_prefix": "TenantACME",
                "folder": "Tenant/ACME",
                "namespace_path": "Tenant\\ACME",
                "route_file": "tenant.php",
                "route_prefix": "acme",
                "route_name": "acme.",
                "permission_prefix": "acme",
                "route_middleware": ["web", "auth", "tenant-auth"]
            }
        ]
    }
}
```

> El array `tenant` puede contener **múltiples entradas**, una por cada tenant específico del proyecto. Cada entrada genera su propio espacio de nombres, carpetas y marcador de rutas aislado.

### Claves del contexto `tenant_shared` con `route_prefix: null`

[](#claves-del-contexto-tenant_shared-con-route_prefix-null)

Es el único contexto sin prefijo de URL ni de nombre de ruta. `contextRoute('roles.index')` devuelve simplemente `'roles.index'` — diseñado para código estándar que se ejecuta bajo el dominio de cada tenant.

---

🌳 Estructura de árbol de un módulo generado
-------------------------------------------

[](#-estructura-de-árbol-de-un-módulo-generado)

El siguiente árbol corresponde a `innodite:make-module User --context=central` (módulo completo, 24 archivos).

**Patrón v3.5.x:** Los componentes principales siguen `{Tipo}/{Contexto}/{Entidad}/{Archivo}`.

```
Modules/
└── User/
    ├── Http/
    │   ├── Controllers/
    │   │   └── Central/
    │   │       └── User/
    │   │           └── CentralUserController.php      (RendersInertiaModule + JSON)
    │   └── Requests/
    │       └── Central/
    │           └── User/
    │               ├── CentralUserStoreRequest.php
    │               └── CentralUserUpdateRequest.php
    ├── Models/
    │   └── Central/
    │       └── User/
    │           └── CentralUser.php                    (con $table definida)
    ├── Services/
    │   ├── Central/
    │   │   └── User/
    │   │       └── CentralUserService.php
    │   └── Contracts/
    │       └── Central/
    │           └── User/
    │               └── CentralUserServiceInterface.php
    ├── Repositories/
    │   ├── Central/
    │   │   └── User/
    │   │       └── CentralUserRepository.php
    │   └── Contracts/
    │       └── Central/
    │           └── User/
    │               └── CentralUserRepositoryInterface.php
    ├── Providers/
    │   └── UserServiceProvider.php                (binding automático Interface↔Implementation)
    ├── Database/
    │   ├── Migrations/
    │   │   └── Central/
    │   │       └── User/
    │   │           └── *_create_users_table.php   (migración anónima)
    │   ├── Seeders/
    │   │   └── Central/
    │   │       └── User/
    │   │           └── CentralUserSeeder.php
    │   └── Factories/
    │       └── Central/
    │           └── User/
    │               └── CentralUserFactory.php
    ├── Tests/
    │   ├── Feature/
    │   │   └── Central/
    │   │       └── CentralUserTest.php
    │   ├── Unit/
    │   │   └── Central/
    │   │       └── CentralUserServiceTest.php
    │   └── Support/
    │       └── Central/
    │           └── CentralUserSupport.php
    ├── Resources/
    │   └── js/
    │       └── Pages/
    │           └── Central/
    │               ├── CentralUserIndex.vue       (lista paginada, axios.get)
    │               ├── CentralUserCreate.vue      (formulario, axios.post)
    │               ├── CentralUserEdit.vue        (formulario, axios.get + axios.put)
    │               └── CentralUserShow.vue        (detalle, axios.get)
    ├── Jobs/
    │   └── Central/
    │       └── CentralUserExportJob.php
    ├── Notifications/
    │   └── Central/
    │       └── CentralUserWelcomeNotification.php
    ├── Console/
    │   └── Commands/
    │       └── Central/
    │           └── CentralUserCleanupCommand.php
    ├── Exceptions/
    │   └── Central/
    │       └── CentralUserNotFoundException.php
    └── Routes/
        └── web.php                                (rutas CRUD — referencia local)

```

Con `innodite:add-entity User Role --context=central`, se añade dentro de `Modules/User/` una subcarpeta `Role/` paralela a `User/` en cada tipo de componente.

---

📐 Convenciones de nomenclatura
------------------------------

[](#-convenciones-de-nomenclatura)

ContextoPrefijo de claseEjemplo VueEjemplo PHP`central``Central``CentralUserIndex.vue``CentralUserController.php``shared``Shared``SharedInvoiceIndex.vue``SharedInvoiceService.php``tenant_shared``TenantShared``TenantSharedRoleIndex.vue``TenantSharedRoleRepository.php``tenant` (INNODITE)`TenantINNODITE``TenantINNODITEUserIndex.vue``TenantINNODITEUserController.php`**Reglas adicionales:**

- El nombre del módulo siempre va en PascalCase (ej: `User`, `InvoiceItem`, `TaxReport`)
- Las migraciones son anónimas (`return new class extends Migration`) para evitar colisiones de nombres
- Los ServiceProviders llevan el nombre del módulo sin prefijo de contexto (`UserServiceProvider`, no `CentralUserServiceProvider`)
- Los Seeders, Jobs, Notifications y Commands **sí llevan prefijo de contexto** a partir de v3.1.0

---

🔀 Flujo de inyección de rutas
-----------------------------

[](#-flujo-de-inyección-de-rutas)

### Marcadores en `routes/web.php`

[](#marcadores-en-routeswebphp)

```
// Al final del archivo, por contexto central y shared-web:
// {{CENTRAL_ROUTES_END}}
```

### Marcadores en `routes/tenant.php`

[](#marcadores-en-routestenantphp)

```
// Por contexto tenant_shared y shared-tenant:
// {{TENANT_SHARED_ROUTES_END}}

// Por cada tenant específico (uno por tenant, basado en class_prefix):
// {{TENANT_INNODITE_ROUTES_END}}
// {{TENANT_ACME_ROUTES_END}}
```

### Proceso interno de inyección

[](#proceso-interno-de-inyección)

```
1. resolveMarkerKey()   → contexto + route_file → clave del marcador
                          central + web.php         → CENTRAL_ROUTES_END
                          innodite + tenant.php      → TENANT_INNODITE_ROUTES_END

2. blockExists()        → busca firma del bloque existente
                          si ya existe: OMITE (operación idempotente)

3. detectIndentation()  → inspecciona el archivo destino
                          preserva espacios o tabs del estilo existente

4. ensureUseStatement() → verifica que existe `use App\Http\Controllers\...`
                          inserta el `use` si no está presente

5. buildBlock()         → genera el grupo de 7 rutas CRUD con comentario de cabecera

6. str_replace()        → inserta el bloque inmediatamente antes del marcador
                          el marcador permanece en su lugar para futuros módulos

```

### Contexto `shared` — Dualidad de rutas

[](#contexto-shared--dualidad-de-rutas)

Archivo destinoPrefijo URLNombre de rutaMarcador`routes/web.php``central/shared``central.shared.``{{CENTRAL_ROUTES_END}}``routes/tenant.php``tenant/shared``tenant.shared.``{{TENANT_SHARED_ROUTES_END}}`---

📋 Resumen de todos los comandos
-------------------------------

[](#-resumen-de-todos-los-comandos)

ComandoDescripción`innodite:make-module {Name}`Genera módulo completo con backend, vistas Vue y rutas`innodite:add-entity {Module} {Entity}`Agrega una entidad a un módulo existente`innodite:module-setup`Inicializa configuración del paquete en el proyecto`innodite:module-check`Diagnóstico de configuración, permisos y conflictos`innodite:check-env`Verifica integración frontend-backend (bridge Inertia)`innodite:publish-frontend`Publica composables Vue 3 (`useModuleContext`, `usePermissions`)`innodite:migrate-plan`Ejecuta migraciones/seeders por manifiesto y orden explícito`innodite:migrate-one`Ejecuta una migración puntual por coordenada`innodite:seed-one`Ejecuta un seeder puntual por coordenada`innodite:migration-sync`Escanea módulos y sincroniza faltantes en manifiestos`innodite:test-module`Ejecuta tests de módulos con contexto y coverage (HTML, Text, Clover)`innodite:test-sync`Sincroniza `Modules/{Modulo}/Tests/test-config.json` desde `contexts.json``vendor:publish --tag=module-maker-config`Publica `make-module.php``vendor:publish --tag=module-maker-stubs`Publica stubs contextuales personalizables`vendor:publish --tag=module-maker-contexts`Publica `contexts.json` de ejemplo`vendor:publish --tag=module-maker-frontend`Publica composables Vue 3---

📊 Auditoría
-----------

[](#-auditoría)

`storage/logs/module_maker.log` — formato NDJSON (una entrada JSON por línea):

```
{"timestamp":"2026-04-01T12:00:00+00:00","event":"module.created","package":"innodite/laravel-module-maker","version":"3.1.0","module":"User","context_key":"central","context_name":"App Central","routes":true}
```

EventoCuándo se registra`module.created`Módulo completo generado correctamente`module.components`Componentes individuales añadidos a módulo existente`routes.injected`Rutas inyectadas exitosamente en el proyecto`module.rollback`Rollback ejecutado tras error durante la generación```
// Acceso programático al log
ModuleAuditor::readLog();  // devuelve array de entradas
ModuleAuditor::logPath();  // devuelve ruta absoluta al archivo de log
```

---

🧪 Pruebas
---------

[](#-pruebas)

```
composer test           # todos los tests
composer test:unit      # solo unitarios
composer test:feature   # solo integración
composer test:coverage  # con cobertura HTML en /coverage
```

Los tests generados por `make-module` se ubican en:

- `Modules/{Name}/Tests/Feature/{Context}/` — tests de integración HTTP
- `Modules/{Name}/Tests/Unit/{Context}/` — tests unitarios del servicio
- `Modules/{Name}/Tests/Support/{Context}/` — helpers y factories de test

---

📏 Estándares de código
----------------------

[](#-estándares-de-código)

```
composer lint         # verificar PSR-12
composer lint:fix     # corregir automáticamente
composer lint:strict  # verificar declaraciones strict_types
```

El paquete incluye configuración de PHP CS Fixer compatible con PSR-12. Todos los archivos PHP generados incluyen `declare(strict_types=1)` por defecto.

---

📦 Publicar en Packagist / repositorio privado
---------------------------------------------

[](#-publicar-en-packagist--repositorio-privado)

### Repositorio público (Packagist)

[](#repositorio-público-packagist)

```
git init && git add . && git commit -m "feat: release v3.1.0"
git tag v3.1.0 && git push origin main --tags
```

Luego registrar el repositorio en [packagist.org](https://packagist.org) con la URL del repositorio.

### Repositorio privado (VCS)

[](#repositorio-privado-vcs)

Agregar en el `composer.json` del proyecto consumidor:

```
{
    "repositories": [
        {
            "type": "vcs",
            "url": "https://github.com/innodite/laravel-module-maker"
        }
    ]
}
```

```
composer require innodite/laravel-module-maker:^3.5
```

---

📝 Changelog
-----------

[](#-changelog)

Ver [CHANGELOG.md](CHANGELOG.md) para el historial completo de versiones.

---

📄 Licencia
----------

[](#-licencia)

MIT — [Anthony Filgueira](https://www.innodite.com)

###  Health Score

45

—

FairBetter than 91% of packages

Maintenance85

Actively maintained with recent releases

Popularity15

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity62

Established project with proven stability

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~5 days

Total

50

Last Release

77d ago

Major Versions

v1.0.1 → v2.0.02026-03-28

v2.5.1 → v3.0.0-rc12026-04-01

### Community

Maintainers

![](https://www.gravatar.com/avatar/ec8225d319cf993bc93da7fcaa016602793b3c1fec6677fe0d9dd10b77a99f47?d=identicon)[afilgueira](/maintainers/afilgueira)

---

Top Contributors

[![AnthonyFilgueira](https://avatars.githubusercontent.com/u/3899819?v=4)](https://github.com/AnthonyFilgueira "AnthonyFilgueira (156 commits)")

---

Tags

laravelgeneratorscaffoldartisanmoduledddmulti-tenanttenancy

###  Code Quality

TestsPest

Code StylePHP\_CodeSniffer

### Embed Badge

![Health badge](/badges/innodite-laravel-module-maker/health.svg)

```
[![Health](https://phpackages.com/badges/innodite-laravel-module-maker/health.svg)](https://phpackages.com/packages/innodite-laravel-module-maker)
```

###  Alternatives

[laravel/ai

The official AI SDK for Laravel.

9782.1M161](/packages/laravel-ai)[psalm/plugin-laravel

Psalm plugin for Laravel

3345.1M337](/packages/psalm-plugin-laravel)[laravel/mcp

Rapidly build MCP servers for your Laravel applications.

76518.2M118](/packages/laravel-mcp)[laravel/surveyor

Static analysis tool for Laravel applications.

8690.3k12](/packages/laravel-surveyor)[erag/laravel-lang-sync-inertia

A powerful Laravel package for syncing and managing language translations across backend and Inertia.js (Vue/React) frontends, offering effortless localization, auto-sync features, and smooth multi-language support for modern Laravel applications.

4821.5k](/packages/erag-laravel-lang-sync-inertia)[aedart/athenaeum

Athenaeum is a mono repository; a collection of various PHP packages

245.2k](/packages/aedart-athenaeum)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
