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.
Requires
- php: ^8.2
- illuminate/console: ^11.0|^12.0
- illuminate/filesystem: ^11.0|^12.0
- illuminate/routing: ^11.0|^12.0
- illuminate/support: ^11.0|^12.0
Requires (Dev)
- orchestra/testbench: ^9.0|^10.0
- pestphp/pest: ^3.0
- pestphp/pest-plugin-laravel: ^3.0
- squizlabs/php_codesniffer: ^3.10
- dev-main
- v3.5.6
- v3.5.5
- v3.5.4
- v3.5.3
- v3.5.2
- v3.5.1
- v3.5.0
- v3.4.6
- v3.4.5
- v3.4.4
- v3.4.3
- v3.4.2
- v3.4.1
- v3.4.0
- v3.3.9
- v3.3.8
- v3.3.7
- v3.3.6
- v3.3.5
- v3.3.4
- v3.3.3
- v3.3.2
- v3.3.1
- v3.3.0
- v3.2.9
- v3.2.6
- v3.2.5
- v3.2.4
- v3.2.3
- v3.2.2
- v3.2.1
- v3.2.0
- v3.1.1
- v3.1.0
- v3.0.6
- v3.0.5
- v3.0.4-rc1
- v3.0.3-rc1
- v3.0.2-rc1
- v3.0.1
- v3.0.1-rc1
- v3.0.0-rc1
- v2.5.1
- v2.4.0
- v2.3.0
- v2.2.1
- v2.2.0
- v2.1.0
- v2.0.0
- v1.0.1
- dev-docs/github-pages-comprehensive-commands
This package is auto-updated.
Last update: 2026-04-06 20:26:33 UTC
README
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
Se consideran deprecados los tags históricos con referencias heredadas a software/proyecto externo.
Tags deprecados:
v2.5.0v3.2.7av3.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
- Requisitos
- Instalación
- Tabla comparativa de contextos
- Versiones Deprecadas
- Arquitectura Frontend
- Guía de comandos
- Archivos generados por contexto
- Flujo completo por contexto
- Composables Vue 3
- Stubs contextuales
- Bridge Frontend-Backend
- Estructura de contextos
- Estructura de árbol de un módulo generado
- Convenciones de nomenclatura
- Flujo de inyección de rutas
- Auditoría
- Pruebas
- Estándares de código
- Publicar en Packagist
- Changelog
- Licencia
Nuevos en v3.5.x:
- Subcarpeta por entidad — patrón
{Tipo}/{Contexto}/{Entidad}/ innodite:add-entity— nuevo comando para módulos multi-entidad
✅ Requisitos
| Dependencia | Versión mínima |
|---|---|
| PHP | 8.2+ |
| Laravel | 11.0+ |
| illuminate/support | ^11.0|^12.0 |
| illuminate/console | ^11.0|^12.0 |
| illuminate/filesystem | ^11.0|^12.0 |
| illuminate/routing | ^11.0|^12.0 |
| @inertiajs/vue3 | ^1.0 (frontend) |
| Vue | ^3.0 (frontend) |
Compatible opcionalmente con
stancl/tenancyyspatie/laravel-permission.
🚀 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)
php artisan innodite:module-setup
Crea module-maker-config/ en la raíz del proyecto con:
contexts.json— Definición de contextos y tenantsstubs/contextual/— Plantillas PHP y Vue personalizables
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
Los 4 contextos disponibles cubren todos los escenarios de un proyecto multi-tenant:
| Contexto key | Prefijo de clase | Carpeta PHP | Carpeta Vue | Archivo de rutas | Nombre de ruta ejemplo | Archivos 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 enweb.php. PrefijoCentral.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 encontexts.json, cada entrada genera su propio espacio aislado.
🖥️ Arquitectura Frontend
Regla fundamental — No negociable en este paquete.
| Responsabilidad | Tecnología |
|---|---|
| Navegación entre páginas | Inertia.js (router.visit(), router.get()) |
| Carga y mutación de datos | axios (GET, POST, PUT, DELETE) |
| Contexto activo y permisos | Props 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
innodite:make-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:
| Flag | Componente 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
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]
| Argumento | Descripció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
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
php artisan innodite:module-check
Verifica el entorno del proyecto e informa sobre:
contexts.json— validez, estructura y claves requeridas- Permisos de escritura en
Modules/,routes/,storage/logs/ - Colisiones de nombres entre módulos y ServiceProviders
- Últimas 5 entradas del log de auditoría
innodite:check-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:
- Modelo User —
HasRoles(Spatie) oInnoditeUserPermissions HandleInertiaRequests—auth.permissionscompartidoInnoditeContextBridge— registrado en el stack web
innodite:publish-frontend — Composables Vue 3
php artisan innodite:publish-frontend
php artisan innodite:publish-frontend --force # sobreescribir
Publica en resources/js/Composables/:
useModuleContext.jsusePermissions.js
innodite:migrate-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_...phpRoles:Tenant/Shared/TenantSharedRoleSeeder→Modules\Roles\Database\Seeders\Tenant\Shared\TenantSharedRoleSeeder
Qué valida el comando:
- Que el manifiesto exista y sea JSON válido
- Que
migrationsyseederssean 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
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:
- Resuelve la ruta exacta del archivo de migración desde la coordenada.
- Detecta automáticamente el manifiesto objetivo según el contexto.
- Si la coordenada aplica a múltiples manifiestos, muestra los destinos y pide confirmación.
- Muestra antes de ejecutar:
- Tipo: migración
- Coordenada
- Conexión
- Base de datos
- Manifiesto destino
- Ruta real del archivo
- Si la coordenada no está registrada en el manifiesto, la agrega primero.
- Ejecuta solo la migración indicada.
Reglas de resolución:
Central=>central_order.jsonShared=> puede aplicar acentral_order.jsony a los manifiestos tenantTenant/Shared=> aplica a todos los manifiestos tenantTenant/X=> aplica al manifiestotenant_x_order.jsoncorrespondiente
Importante:
- Requiere confirmación interactiva antes de ejecutar, salvo que uses
--yes. - En
--dry-runno 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
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:
- Resuelve el FQCN (clase completa) del seeder desde la coordenada.
- Detecta automáticamente el manifiesto objetivo según el contexto.
- Si la coordenada aplica a múltiples manifiestos, muestra los destinos y pide confirmación.
- Muestra antes de ejecutar:
- Tipo: seeder
- Coordenada
- Conexión
- Base de datos
- Manifiesto destino
- Clase real que va a ejecutar
- Si la coordenada no está registrada en el manifiesto, la agrega primero.
- Ejecuta solo el seeder indicado.
Reglas de resolución:
Central=>central_order.jsonShared=> puede aplicar acentral_order.jsony a los manifiestos tenantTenant/Shared=> aplica a todos los manifiestos tenantTenant/X=> aplica al manifiestotenant_x_order.jsoncorrespondiente
Importante:
- Requiere confirmación interactiva antes de ejecutar, salvo que uses
--yes. - En
--dry-runno 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
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:
- Si no envías
--manifest, leemodule-maker-config/contexts.jsony propone:central_order.jsontenant_{permission_prefix}_order.jsonpor cada tenant configurado.
- Pide confirmación en consola antes de generar/sincronizar múltiples manifiestos (omite prompt con
--yes). - Crea
module-maker-config/migrations/si no existe. - Crea cada manifiesto faltante (estructura vacía).
- Escanea:
Modules/*/Database/Migrations/**Modules/*/Database/Seeders/**
- Convierte hallazgos a coordenadas.
- Filtra por alcance de manifiesto:
central_order.json=> contextosCentralyShared.tenant_*.json=>Shared+Tenant/Shared+ contextoTenant/{X}del tenant objetivo.
- 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
# 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
seederprevio 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)
- Clover XML →
- HTML →
- ✅ Valida que Xdebug o PCOV estén activos para coverage
- ✅ Muestra tabla resumen con resultados y porcentaje de cobertura
- ✅ Soporta flag
--filterde PHPUnit para tests específicos - ✅ Detección automática de módulos sin tests (warning + continuar)
innodite:test-sync — Sincronizar Tests/test-config.json
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.jsonsi no existe - ✅ Agrega contextos faltantes sin duplicar
- ✅ Conserva overrides manuales de
db_connection,db_database,seeder,enabledyenv - ✅ No asume ninguna base de datos por defecto: tú defines
db_connectionydb_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
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
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
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
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
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
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
php artisan innodite:make-module User --context=central
Ruta inyectada en routes/web.php
// 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()
contextRoute('users.index') // Resuelve: 'central.users.index'
Contexto shared
php artisan innodite:make-module Invoice --context=shared
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
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
php artisan innodite:make-module Role --context=tenant_shared
Ruta inyectada en routes/tenant.php
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)
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
// 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()
contextRoute('products.index') // Resuelve: 'innodite.products.index'
🧩 Composables Vue 3
Los composables se publican con php artisan innodite:publish-frontend en resources/js/Composables/.
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
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.
<template> <!-- Botón visible solo si el usuario tiene permiso --> <button v-if="can('users.create')" @click="goToCreate()"> Nuevo usuario </button> <!-- Acciones de fila protegidas por permisos --> <button v-if="can('users.edit')" @click="edit(item.id)">Editar</button> <button v-if="can('users.delete')" @click="destroy(item.id)">Eliminar</button> </template>
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
<script setup> 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()) </script>
Ejemplo — CentralUserCreate.vue
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
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
idcomo prop de Inertia (nunca el objeto completo) - Carga el registro vía axios al montarse
🔧 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
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
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
| Variable | Descripción | Ejemplo |
|---|---|---|
{{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
Middleware InnoditeContextBridge
Intercepta cada request e inyecta vía Inertia::share():
| Prop | Valor 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:
- Spatie Permission →
$user->getAllPermissions()->pluck('name') InnoditeUserPermissions→$user->getInnoditePermissions()- 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
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)
{
"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
tenantpuede 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
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
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
| Contexto | Prefijo de clase | Ejemplo Vue | Ejemplo 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, noCentralUserServiceProvider) - Los Seeders, Jobs, Notifications y Commands sí llevan prefijo de contexto a partir de v3.1.0
🔀 Flujo de inyección de rutas
Marcadores en routes/web.php
// Al final del archivo, por contexto central y shared-web: // {{CENTRAL_ROUTES_END}}
Marcadores en routes/tenant.php
// 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
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
| Archivo destino | Prefijo URL | Nombre de ruta | Marcador |
|---|---|---|---|
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
| Comando | Descripció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
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}
| Evento | Cuá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
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 HTTPModules/{Name}/Tests/Unit/{Context}/— tests unitarios del servicioModules/{Name}/Tests/Support/{Context}/— helpers y factories de test
📏 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
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 con la URL del repositorio.
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
Ver CHANGELOG.md para el historial completo de versiones.
📄 Licencia
MIT — Anthony Filgueira