Manual · Laravel Module Maker
El generador del patrón
Escribe el módulo entero —modelo, repositorio, servicio, controlador,
validación, migración, seeders, rutas con el permiso de cada una, pantallas y pruebas— con
un solo comando, y con la misma forma en una aplicación única y en multiinquilino.
Un módulo agrupa varias subfuncionalidades y cada una es
autocontenida: Modules/<Módulo>/<SubFuncionalidad>/<Capa>/<Contexto>/.
La capa va dentro de la subfuncionalidad y el contexto es la hoja, así que leer, mover o
borrar una funcionalidad es leer, mover o borrar una carpeta.
Por dónde empezar
Instalación, en dos líneas
composer require innodite/laravel-module-maker:^5.0
php artisan innodite:module-setup
Y cuando algo no salga como esperabas, php artisan innodite:doctor: diagnostica
en cascada y cada fallo trae la línea que lo arregla.
El paquete se instala con Composer y necesita un solo paso más antes de poder generar nada.
composer require innodite/laravel-module-maker:^5.0
php artisan innodite:module-setup
La versión es la 5.0, y cambia la forma de todo lo que el paquete escribe: un módulo generado con una 4.x no coincide con lo que genera esta, ni en carpetas ni en namespaces.
module-setup es lo que elige el modo del proyecto y deja escrita la configuración. Sin modo elegido los comandos se niegan a generar y dicen cómo elegirlo — eso es deliberado, y el porqué está en la ficha del modo.
Comprobar que va a funcionar
php artisan innodite:doctor
⭐ Empieza siempre por aquí cuando algo no salga como esperabas. Es un diagnóstico en cascada: primero el entorno del generador —si puede escribir, si su configuración está publicada—, después el contrato de tu proyecto: que el modelo de usuario sepa resolver permisos, que el puente de contexto esté registrado y que estén las piezas de las que dependen las pantallas generadas.
Cada fallo trae su línea de arreglo. Si el entorno falla, la segunda etapa no se ejecuta y el comando lo dice; para verlo todo de una vez, --continuar.
Lo que tu proyecto tiene que traer
El paquete se escribe contra las convenciones de tu proyecto, no las instala:
| Necesitas | Para qué |
|---|
| Las tablas de permisos y roles | El generador escribe los permisos de cada ruta; las tablas son tuyas |
Ziggy, con @routes en el layout | Las pantallas generadas piden sus rutas por su nombre |
El namespace Modules\ en tu autoload.psr-4 | Sin él, nada de lo generado resuelve |
⚠️ Las tres fallan en silencio si faltan, y por eso el doctor las mira. La más traicionera es la segunda: sin ella la pantalla se genera perfecta y muere al abrirse, con un error de JavaScript que no menciona ni al módulo ni al paquete.
El modo decide la forma de todo lo que se genera, y se elige una vez, al instalar.
| Modo | Cuándo | Qué cambia en lo generado |
|---|
single-app | Una aplicación, una base de datos | Sin contextos: --context no se pasa |
multitenant | Una aplicación central y varios inquilinos | Cada archivo vive bajo la carpeta de su contexto y lleva su prefijo |
php artisan innodite:module-setup --mode=single-app
php artisan innodite:module-setup --mode=multitenant --tenancy=stancl
⛔ Ningún comando adivina el modo
Y ninguno asume uno por defecto: sin modo elegido se niegan a generar y dicen cómo elegirlo.
Es la decisión de diseño más importante del paquete. Un valor por defecto puesto «para que nadie note nada» no produce un error: produce una estructura equivocada, multiplicada por cada módulo que se genere a partir de ahí, y descubierta mucho después.
En multiinquilino hay dos contextos, y solo dos
| Contexto | Dónde vive | Archivo de rutas | Permiso | ¿Declara conexión el modelo? |
|---|
central | La aplicación que administra a los demás | web.php | central-permission | Sí |
tenant | Lo que corre dentro de cada inquilino | tenant.php | tenant-permission | No |
⭐ Y esa última columna es la que más cara sale equivocar. El modelo del inquilino no nombra su conexión, porque quien la conmuta es el paquete de tenencia al identificar la petición. Escribirla ahí ata el modelo a una base concreta y rompe exactamente lo que protege.
Si tu proyecto necesita otro contexto, lo declara
El paquete trae dos de fábrica y no inventa más. Un proyecto que necesite uno propio —un eje de informes, una consola de soporte— lo añade a su module-maker-config/contexts.json con lo que ese contexto es:
"reporting": {
"id": "reporting",
"is_tenant": false,
"folder": "Reporting",
"route_file": "web.php",
"connection_key": "reporting",
"permission_prefix": "reporting_",
"permission_middleware": "reporting-permission"
}
Y a partir de ahí --context=reporting genera como cualquier otro: la carpeta, el prefijo de las clases, el archivo de rutas, el permiso de cada una y la conexión salen de lo que declaraste, no de una lista escrita dentro del paquete.
⚠️ Lo que no puedes es quitar central ni tenant: el diagnóstico los sigue reclamando. Un contexto propio se añade, no sustituye.
El paquete de tenencia también se declara
php artisan innodite:module-setup --mode=multitenant --tenancy=stancl
Los archivos de rutas de un proyecto multiinquilino necesitan una envoltura, y esa envoltura está escrita en el vocabulario del paquete de tenencia concreto. Si el tuyo no está soportado, declara --tenancy=none: el generador no inventa la envoltura y deja el archivo con una nota que dice dónde va y qué haría, en vez de escribir algo que parece correcto y no lo es.
Lo que el paquete escribe tiene una sola forma, y conviene leerla una vez:
Modules/<Módulo>/<SubFuncionalidad>/<Capa>/<Contexto>/<Archivo>
Manda la subfuncionalidad, la capa va dentro de ella y el contexto es la hoja. En aplicación única el último tramo no existe y el resto es idéntico, así que los dos modos comparten estructura en vez de parecerse.
Un módulo generado, de verdad
Esto es lo que deja innodite:make-module Invoice --context=central, medido sobre la ejecución real:
Modules/Invoice/
├── Docs/ ← architecture · history · schema
├── Routes/web.php
├── Providers/Central/CentralInvoiceServiceProvider.php
├── Database/Seeders/Application/Central/ ← los tres maestros del módulo
│
└── Invoice/ ← la subfuncionalidad
├── Models/Central/CentralInvoice.php
├── Http/Controllers/Central/CentralInvoiceController.php
├── Http/Requests/Central/CentralInvoiceStoreRequest.php
├── Http/Requests/Central/CentralInvoiceUpdateRequest.php
├── Services/Central/CentralInvoiceService.php
├── Services/Contracts/Central/CentralInvoiceServiceInterface.php
├── Repositories/Central/CentralInvoiceRepository.php
├── Repositories/Contracts/Central/CentralInvoiceRepositoryInterface.php
├── Database/Migrations/Central/…_create_invoices_table_final.php
├── Database/Seeders/Central/ ← las seis piezas de datos
├── Database/Factories/Central/CentralInvoiceFactory.php
├── resources/js/Pages/Central/ ← Index · Show · Create · Edit
├── resources/js/__tests__/Central/
└── Tests/Feature/Central/ ← las siete piezas del contrato
37 archivos. Y en aplicación única salen los mismos 37, sin el tramo Central/ y sin el prefijo en los nombres: Invoice/Services/InvoiceService.php.
Qué queda al nivel del módulo, y por qué
Cuatro cosas no pertenecen a ninguna subfuncionalidad concreta:
| |
|---|
Docs/ | Documenta el módulo entero |
Routes/ | Un archivo por contexto — web.php para el central, tenant.php para el inquilino |
Providers/<Contexto>/ | Uno por contexto. Con uno solo, ese archivo era el único sitio del módulo donde los contextos se mezclaban, justo el que decide qué implementación se inyecta |
Database/Seeders/Application/<Contexto>/ | Los tres maestros que levantan el módulo entero |
Por qué la subfuncionalidad va delante
Con la capa por delante —Http/Controllers/Central/User/…—, ver qué tiene una subfuncionalidad obligaba a abrir doce carpetas; y con el contexto por delante, cada capa duplicaba su rama entera por contexto.
Con este orden, una subfuncionalidad es autocontenida: se lee, se mueve y se borra de una pieza. Y añadir un contexto no duplica el árbol, solo añade una hoja.
Los contratos también viven en su contexto
Invoice/Services/Contracts/Central/CentralInvoiceServiceInterface.php
La carpeta del contexto está —es la hoja, aquí como en todas las capas—, y Contracts/ se intercala antes. Cada implementación tiene su contrato en su contexto, que es lo que permite inyectar una u otra según dónde corra el código.
Diez comandos, y el orden en que se usan es casi siempre el mismo.
| Comando | Para qué |
|---|
innodite:doctor | Empieza por aquí. Diagnóstico en cascada, con la línea de arreglo de cada fallo |
innodite:module-setup | Elige el modo del proyecto y deja escrita la configuración |
innodite:make-module | Genera un módulo completo y lo declara en el orden de despliegue |
innodite:add-entity | Añade una subfuncionalidad a un módulo que ya existe |
innodite:deploy | Levanta el proyecto: esquema, datos canónicos y permisos, en el orden declarado |
innodite:migrate-one | Aplica una migración concreta, contra la base de su contexto |
innodite:crear-bd-test | Clona el esquema real en la base de pruebas, sin una sola fila |
innodite:test | Ejecuta el contrato de pruebas de una subfuncionalidad |
innodite:publish-stubs | Exporta al proyecto solo las plantillas que vayas a personalizar |
innodite:migrate-plan | Retirado. Sigue registrado solo para decirlo |
El contexto, en los que generan
En multiinquilino, make-module, add-entity, test, deploy y migrate-one piden --context. Se pasa central, tenant o el que tu proyecto haya declarado en su catálogo.
En aplicación única no se pasa: no hay eje que elegir, y el comando lo rechaza diciéndolo.
php artisan innodite:make-module Invoice --context=central # multiinquilino
php artisan innodite:make-module Invoice # aplicación única
⛔ Y ninguno adivina el contexto cuando falta. Caer en el primero del catálogo acertaría a veces y el resto de las veces escribiría las rutas en el archivo que no es, protegidas con el permiso de otro contexto: un módulo perfectamente escrito y completamente mal.
El ensayo, en casi todos
php artisan innodite:make-module Invoice --dry-run
php artisan innodite:deploy stage --dry-run
--dry-run enseña lo que haría sin escribir nada. En deploy dice además contra qué conexión iría, que es la pregunta que más caro sale equivocar.
⛔ innodite:migrate-plan está retirado
Sigue registrado, pero solo para decirlo: el esquema lo aplica innodite:deploy, a través de los seeders. Si tienes un guion que lo llama, cámbialo.
Se retiró porque ordenaba las migraciones por el nombre de las carpetas en vez de por qué tabla depende de cuál, así que una tabla con clave foránea podía aplicarse antes que aquella a la que apunta. El orden bueno ya vivía en otro sitio —la lista que declara tu proyecto— y tener dos dueños del mismo orden era el defecto.
Un módulo no está listo porque sus archivos existan
Está listo cuando se despliega y su contrato de pruebas pasa:
php artisan innodite:doctor
php artisan innodite:deploy stage
php artisan innodite:test Invoice Payment
⭐ Es la diferencia entre comprobar que se escribieron los archivos y comprobar que hacen efecto — y es donde aparecen los fallos que no se ven leyendo el código generado.
php artisan innodite:make-module Invoice
Escribe el módulo entero —modelo, repositorio, servicio, controlador, form requests, migración, seeders, rutas, pantallas y pruebas— y lo declara en el orden de despliegue, que es lo que hace que un despliegue posterior lo levante.
En multiinquilino hay que decir dónde vive:
php artisan innodite:make-module Invoice --context=central
php artisan innodite:make-module Invoice --context=tenant
⛔ Sin contexto, en un proyecto multiinquilino, no se genera nada y el comando dice por qué. Lo que escribiría sin saberlo sería plausible y equivocado: rutas en el archivo que no toca, protegidas por un permiso que no corresponde.
Y lo que sale es la misma estructura en los dos modos: 37 archivos, con la subfuncionalidad por delante y el contexto como hoja. La forma completa, con el árbol de un módulo real, está en la ficha del árbol.
Añadir una entidad a un módulo que ya existe
php artisan innodite:add-entity Invoice Payment
Respeta lo que ya hay: inyecta lo nuevo en el proveedor del módulo sin sobrescribir sus enlaces, y las pantallas que ya existan no se tocan.
Generar solo una pieza
Las banderas cortas añaden una sola capa, y sirven para reparar algo que falta:
php artisan innodite:make-module Invoice -S # solo el servicio y su interfaz
php artisan innodite:make-module Invoice -R -G # repositorio y migración
Lo que el comando te dice al terminar
Además de lo generado, avisa de dos cosas que harían inútil todo lo anterior:
- - Que el módulo no vaya a cargar, porque falta declarar el namespace en tu autoload o falta
recargarlo. Sin eso la aplicación arranca igual y sus rutas sencillamente no existen.
- - Que su pantalla no vaya a abrir, porque falta el generador de rutas del navegador o su
directiva en el layout.
⭐ Los dos fallan sin dejar rastro en el servidor: la petición responde 200. Por eso se avisan en el momento, y no se descubren horas después.
Desplegar es lo que convierte los archivos generados en tablas, datos y permisos que existen.
php artisan innodite:deploy stage
php artisan innodite:deploy production
Ejecuta los seeders en el orden que declara tu configuración: primero el esquema, después los datos canónicos, después los permisos y el usuario administrador.
En multiinquilino hay que decir a qué cliente
php artisan innodite:deploy stage --context=central
php artisan innodite:deploy stage --context=tenant --tenant=acme
php artisan innodite:deploy stage --context=tenant --all
⛔ Sin --tenant ni --all el comando no arranca. Con clientes que comparten funcionalidad, el despliegue tiene que entrar en el contexto de cada uno. En una petición web eso lo hace el middleware de identificación; en consola no hay middleware que lo haga.
⚠️ Un despliegue que no entra escribe en la base central creyendo que escribe en la del cliente, y lo hace sin un solo aviso.
Un módulo suelto
php artisan innodite:deploy stage --module=Invoice
Levanta solo ese módulo, por sus maestros, en vez del proyecto entero.
Una migración concreta
php artisan innodite:migrate-one Invoice:Central/2026_01_01_crear_invoices.php
La coordenada dice el módulo, el contexto y el archivo, y de ahí sale contra qué base se aplica.
Antes de tocar nada
php artisan innodite:deploy production --dry-run
⭐ En producción, el ensayo es obligatorio en la práctica: dice qué se ejecutaría y contra qué conexión. Es la comprobación que separa un despliegue de un incidente.
Los datos canónicos no se destruyen
Los seeders generados son no destructivos por defecto: reponen lo que falta y respetan lo que hay. El modo que sí borra existe, pero hay que pedirlo a propósito y el comando avisa antes.
Cada subfuncionalidad generada trae su grupo de pruebas, y se ejecuta así:
php artisan innodite:crear-bd-test
php artisan innodite:test Invoice Payment
La base de pruebas es un clon del esquema real, sin filas
crear-bd-test copia el esquema de tu base real a la base de pruebas y no copia ni un dato.
⛔ Re-clonarla es un paso del ciclo, no algo que ocurra en cada corrida. Rehacerla siempre esconde justo lo que hay que ver: una prueba que solo pasa con la base recién hecha está contando algo, y borrar la evidencia antes de leerla lo hace indiagnosticable.
Un rojo se clasifica antes de investigarse
El orden importa, porque investigar un intermitente como si fuera un defecto cuesta horas:
| Sospecha | Cómo se comprueba |
|---|
| La base venía sucia | --reclonar — rehace la base antes de empezar |
| Es intermitente | --repetir=3 — repite la pieza que falló |
| Es un defecto de verdad | --sin-reclonar — conserva el estado para poder mirarlo |
⭐ Y si pasa tras re-clonar, eso es lo que se reporta: «pasó tras re-clonar», no un verde limpio. Son cosas distintas y confundirlas oculta un problema de datos que va a volver.
Acotar y continuar
php artisan innodite:test Invoice Payment --filter=puede_crear
php artisan innodite:test Invoice Payment --continuar
Por defecto el grupo corta en la primera pieza que falla: las siguientes dependen de ella y sus rojos serían ruido. --continuar desactiva ese corte cuando lo que quieres es el panorama completo.
Qué se prueba
Las capas —que el controlador delegue, que solo el repositorio toque el modelo—, los permisos de cada ruta, el esquema, el despliegue y las pantallas. En contexto de cliente, la prueba aparta la identificación por dominio: en una suite no hay dominio de cliente y cada petición moriría con un 404 donde se espera un 403. Lo que se mide es la puerta del permiso.
Lo que el generador escribe sale de unas plantillas, y hay tres formas de cambiarlas.
1 · Los stubs de tu proyecto
php artisan vendor:publish --tag=module-maker-stubs
Copia las plantillas a module-maker-config/stubs/contextual/ y a partir de ahí ganan a las del paquete. Puedes cambiar solo las que te interesen: lo que no esté ahí se sigue leyendo del paquete.
⚠️ Publicarlas tiene un coste que conviene saber: una plantilla copiada no se actualiza con el paquete. Si no vas a personalizarla, no la publiques — y si publicaste todas «por si acaso», borra las que no tocaste.
2 · Los stubs que aporta otro paquete instalado
Cualquier paquete instalado que traiga
stubs/module-maker/contextual/<nombre>.stub
participa automáticamente. Sirve para generar contra una biblioteca de interfaz concreta —su tabla, sus formularios— en vez de contra las plantillas genéricas.
Quién gana a quién, de lo más específico a lo más genérico:
| # | Dónde | Manda |
|---|
| 1 | module-maker-config/stubs/contextual/{Contexto}/ | Tu proyecto, para un contexto |
| 2 | module-maker-config/stubs/contextual/ | Tu proyecto |
| 3 | stubs/module-maker/contextual/ de un paquete instalado | Ese paquete |
| 4 | Las del generador | El paquete |
⭐ Tu proyecto siempre gana. Es el único que puede tener la última palabra sobre su propio código. Y si dos paquetes aportan la misma plantilla, gana el primero por orden alfabético y make-module lo dice al terminar, junto con quién aportó qué.
3 · El punto de enganche del módulo
provider-boot.stub es una plantilla opcional: no existe en el paquete y solo se usa si tu proyecto o un paquete instalado la aportan. Su contenido se escribe dentro del boot() del proveedor del módulo generado; si no la aporta nadie, ese boot() sale vacío.
Está pensada para enganchar el módulo recién creado en el menú de tu aplicación, que es lo que el generador no puede escribir por su cuenta: esa línea depende de cómo declare su menú cada proyecto. Recibe dos variables:
| Variable | Qué trae | Ejemplo |
|---|
{{{ moduleName }}} | El nombre del módulo | Invoice |
{{{ functionality }}} | La funcionalidad, como la nombran sus rutas | invoices |
⛔ Por qué no viene hecho. Escribir ese enganche «por si acaso» llamaría a una clase que puede no existir, y eso no deja un módulo invisible: deja la aplicación entera sin arrancar, incluido el artisan con el que se arreglaría.
El paquete escribe una estructura concreta, con una opinión detrás. Cuando esa opinión no es la tuya, forzarlo cuesta más que no usarlo.
No lo uses si…
Tu proyecto no separa las capas. El generador escribe siempre la misma cadena: la validación en un form request, el controlador que delega en un servicio, el servicio que pregunta al repositorio y solo el repositorio tocando el modelo. Si tu proyecto consulta el modelo desde el controlador, lo generado va a chocar con lo que ya tienes en cada módulo.
Quieres un CRUD y nada más. Un módulo generado trae seis rutas con su permiso cada una, los seeders que crean esos permisos, la pantalla y su grupo de pruebas. Para una tabla auxiliar de tres campos que solo toca un administrador, es más andamiaje del que vas a mantener.
Tu aplicación no es Laravel con Inertia y Vue. Las pantallas generadas asumen esa combinación. El backend te serviría igual, pero tendrías que tirar las vistas en cada módulo.
Necesitas que un módulo tenga una forma distinta a los demás. El valor del paquete es que todos se parezcan. Si el tuyo es la excepción, escríbelo a mano: personalizar las plantillas para un solo caso convierte la excepción en la norma de todo lo que generes después.
Sí te compensa cuando…
- - Vas a crear varios módulos con la misma forma, y quieres que sigan pareciéndose dentro de un
año.
- - Te importa que cada ruta tenga su permiso y que ese permiso exista de verdad, creado por un
seeder, y no dependa de que alguien se acuerde.
- - Trabajas con varios clientes y necesitas que el mismo módulo se despliegue en el contexto correcto
sin decidirlo a mano cada vez.
Y una advertencia sobre el punto intermedio
⚠️ Generar el módulo y después reescribirlo entero a mano es el peor de los dos mundos. Queda un módulo que parece generado pero ya no lo es: la próxima vez que ejecutes add-entity sobre él, lo que se inyecte no encajará con lo que hay. Si vas a reescribirlo, no lo generes.