Estructura de directorios de la aplicación
¿Cómo diseñar una estructura de directorios clara y escalable para los proyectos en Nette Framework? Le mostraremos prácticas probadas que le ayudarán a organizar el código. Aprenderá:
- cómo estructurar lógicamente la aplicación en directorios
- cómo diseñar la estructura para que escale bien a medida que el proyecto crece
- cuáles son las alternativas posibles y sus ventajas o inconvenientes
Es importante mencionar que Nette Framework en sí no impone ninguna estructura concreta. Está diseñado para adaptarse fácilmente a cualquier necesidad y preferencia.
Estructura básica del proyecto
Aunque Nette Framework no dicta ninguna estructura de directorios fija, existe una disposición predeterminada probada en forma del Web Project:
web-project/ ├── app/ ← directorio de la aplicación ├── assets/ ← archivos SCSS, JS, imágenes..., alternativamente resources/ ├── bin/ ← scripts para la línea de comandos ├── config/ ← configuración ├── log/ ← errores registrados ├── temp/ ← archivos temporales, caché ├── tests/ ← tests ├── vendor/ ← bibliotecas instaladas por Composer └── www/ ← directorio público (document-root)
Puede modificar esta estructura libremente según sus necesidades: renombrar o mover carpetas. Después solo hace falta
ajustar las rutas relativas a los directorios en Bootstrap.php y, eventualmente, en composer.json. Nada
más, ninguna reconfiguración complicada, ningún cambio de constantes. Nette dispone de una autodetección inteligente y
reconoce automáticamente la ubicación de la aplicación, incluida su URL base.
Principios de organización del código
Cuando explora un proyecto nuevo por primera vez, debería poder orientarse rápidamente. Imagine que hace clic en el
directorio app/Model/ y ve esta estructura:
app/Model/ ├── Services/ ├── Repositories/ └── Entities/
De aquí solo se entera de que el proyecto usa unos servicios, unos repositorios y unas entidades. No se entera de nada sobre el propósito real de la aplicación.
Veamos otro enfoque distinto: la organización por dominios:
app/Model/ ├── Cart/ ├── Payment/ ├── Order/ └── Product/
Aquí es diferente: a primera vista está claro que se trata de una tienda online. Los propios nombres de los directorios revelan lo que sabe hacer la aplicación: trabaja con pagos, pedidos y productos.
El primer enfoque (organización por tipo de clase) trae en la práctica varios problemas: el código que está lógicamente relacionado queda fragmentado entre distintas carpetas y hay que ir saltando de una a otra. Por eso organizaremos por dominios.
Espacios de nombres
Es habitual que la estructura de directorios se corresponda con los espacios de nombres de la aplicación. Eso significa que la
ubicación física de los archivos coincide con su espacio de nombres. Por ejemplo, una clase situada en
app/Model/Product/ProductRepository.php debería tener el espacio de nombres App\Model\Product. Este
principio ayuda a orientarse en el código y simplifica la carga automática.
Singular y plural en los nombres
Fíjese en que usamos el singular para los directorios principales de la aplicación: app, config,
log, temp, www. Lo mismo dentro de la aplicación: Model, Core,
Presentation. Es porque cada uno representa un único concepto coherente.
De forma parecida, app/Model/Product representa todo lo relacionado con los productos. No lo llamamos
Products porque no es una carpeta llena de productos (que contendría archivos como nokia.php,
samsung.php). Es un espacio de nombres que contiene clases para trabajar con productos:
ProductRepository.php, ProductService.php.
La carpeta app/Tasks está en plural porque contiene un conjunto de scripts ejecutables independientes:
CleanupTask.php, ImportTask.php. Cada uno de ellos es una unidad autónoma.
Por coherencia, recomendamos usar:
- singular para los espacios de nombres que representan una unidad funcional (aunque trabajen con varias entidades)
- plural para las colecciones de unidades independientes
- en caso de duda, o si no quiere darle vueltas, elija el singular
Directorio público www/
Este directorio es el único accesible desde la web (el document-root). A menudo puede encontrarse con el nombre
public/ en lugar de www/: es solo cuestión de convención y no afecta al funcionamiento de la
aplicación. El directorio contiene:
- el punto de entrada de la aplicación
index.php - el archivo
.htaccesscon las reglas para mod_rewrite (para Apache) - archivos estáticos (CSS, JavaScript, imágenes)
- archivos subidos
Para la correcta seguridad de la aplicación es crucial tener el document-root configurado correctamente.
Nunca coloque en este directorio la carpeta node_modules/: contiene miles de archivos que pueden ser
ejecutables y que no deberían ser accesibles públicamente.
Directorio de la aplicación app/
Este es el directorio principal, el que contiene el código de la aplicación. Estructura básica:
app/ ├── Core/ ← asuntos de infraestructura ├── Model/ ← lógica de negocio ├── Presentation/ ← presenters y plantillas ├── Tasks/ ← scripts de comandos └── Bootstrap.php ← clase de arranque de la aplicación
Bootstrap.php es la clase de arranque de la
aplicación que inicializa el entorno, carga la configuración y crea el contenedor DI.
Veamos ahora con más detalle cada uno de los subdirectorios.
Presenters y plantillas
La parte de presentación de la aplicación está en el directorio app/Presentation. Una alternativa es el más
corto app/UI. Es el lugar de todos los presenters, sus plantillas y las posibles clases auxiliares asociadas.
Esta capa la organizamos por dominios. En un proyecto complejo que combine tienda online, blog y API, la estructura sería así:
app/Presentation/ ├── Shop/ ← frontend de la tienda │ ├── Product/ │ ├── Cart/ │ └── Order/ ├── Blog/ ← blog │ ├── Home/ │ └── Post/ ├── Admin/ ← administración │ ├── Dashboard/ │ └── Products/ └── Api/ ← endpoints de la API └── V1/
Por el contrario, para un blog sencillo usaríamos la siguiente estructura:
app/Presentation/ ├── Front/ ← frontend del sitio web │ ├── Home/ │ └── Post/ ├── Admin/ ← administración │ ├── Dashboard/ │ └── Posts/ ├── Error/ └── Export/ ← RSS, sitemaps, etc.
Carpetas como Home/ o Dashboard/ contienen presenters y plantillas. Carpetas como
Front/, Admin/ o Api/ se llaman módulos. Técnicamente son directorios corrientes
que sirven para dividir lógicamente la aplicación.
Cada carpeta que contiene un presenter incluye el propio archivo del presenter y sus plantillas. Por ejemplo, la carpeta
Dashboard/ contiene:
Dashboard/ ├── DashboardPresenter.php ← presenter └── default.latte ← plantilla
Esta estructura de directorios se refleja en los espacios de nombres de las clases. Por ejemplo,
DashboardPresenter está en el espacio de nombres App\Presentation\Admin\Dashboard (véase Mapeo de presenters):
namespace App\Presentation\Admin\Dashboard;
class DashboardPresenter extends Nette\Application\UI\Presenter
{
// ...
}
Al presenter Dashboard dentro del módulo Admin nos referimos en la aplicación con la notación de
dos puntos como Admin:Dashboard. A su acción default nos referimos entonces como
Admin:Dashboard:default. Con módulos anidados usamos varios grupos de dos puntos, por ejemplo
Shop:Order:Detail:default.
Desarrollo flexible de la estructura
Una de las grandes ventajas de esta estructura es lo elegantemente que se adapta a las necesidades crecientes del proyecto. Tomemos como ejemplo la parte que genera los feeds XML. Al principio tenemos una forma sencilla:
Export/ ├── ExportPresenter.php ← un presenter para todas las exportaciones ├── sitemap.latte ← plantilla para el sitemap └── feed.latte ← plantilla para el feed RSS
Con el tiempo se añaden más tipos de feed y necesitamos más lógica para ellos… ¡Ningún problema! La carpeta
Export/ simplemente se convierte en un módulo:
Export/ ├── Sitemap/ │ ├── SitemapPresenter.php │ └── sitemap.latte └── Feed/ ├── FeedPresenter.php ├── amazon.latte ← feed para Amazon └── ebay.latte ← feed para eBay
Esta transformación es completamente fluida: basta con crear las nuevas subcarpetas, repartir el código en ellas y actualizar
los enlaces (p. ej. de Export:feed a Export:Feed:amazon). Gracias a eso podemos ampliar la estructura
gradualmente según haga falta; el nivel de anidamiento no está limitado de ninguna manera.
Por ejemplo, si en la administración tiene muchos presenters relacionados con la gestión de pedidos, como
OrderDetail, OrderEdit, OrderDispatch, etc., puede crear para una mejor organización un
módulo (carpeta) llamado Order, que contendrá (las carpetas de) los presenters Detail,
Edit, Dispatch y otros.
Ubicación de las plantillas
En los ejemplos anteriores hemos visto que las plantillas están directamente en la carpeta del presenter:
Dashboard/ ├── DashboardPresenter.php ← presenter ├── DashboardTemplate.php ← clase de plantilla opcional └── default.latte ← plantilla
Esta ubicación resulta ser la más cómoda en la práctica: tiene todos los archivos relacionados a mano.
Alternativamente puede colocar las plantillas en una subcarpeta templates/. Nette admite ambas variantes. Incluso
puede colocar las plantillas completamente fuera de la carpeta Presentation/. Todo sobre las opciones de ubicación
de las plantillas lo encontrará en el capítulo Búsqueda de plantillas.
Clases auxiliares y componentes
A los presenters y las plantillas los acompañan a menudo otros archivos auxiliares. Los colocamos de forma lógica según su alcance:
1. Directamente junto al presenter en el caso de componentes específicos de ese presenter:
Product/ ├── ProductPresenter.php ├── ProductGrid.php ← componente para el listado de productos └── FilterForm.php ← formulario para filtrar
2. Para el módulo: recomendamos usar la carpeta Accessory, que queda convenientemente al principio por
orden alfabético:
Front/ ├── Accessory/ │ ├── NavbarControl.php ← componentes para el frontend │ └── TemplateFilters.php ├── Product/ └── Cart/
3. Para toda la aplicación: en Presentation/Accessory/:
app/Presentation/ ├── Accessory/ │ ├── LatteExtension.php │ └── TemplateFilters.php ├── Front/ └── Admin/
Alternativamente puede colocar las clases auxiliares como LatteExtension.php o TemplateFilters.php en
la carpeta de infraestructura app/Core/Latte/. Y los componentes en app/Components. La elección depende
de las convenciones del equipo.
Model: el corazón de la aplicación
El modelo contiene toda la lógica de negocio de la aplicación. La regla para organizarlo es de nuevo: estructurar por dominios:
app/Model/ ├── Payment/ ← todo sobre los pagos │ ├── PaymentFacade.php ← punto de entrada principal │ ├── PaymentRepository.php │ ├── Payment.php ← entidad ├── Order/ ← todo sobre los pedidos │ ├── OrderFacade.php │ ├── OrderRepository.php │ ├── Order.php └── Shipping/ ← todo sobre los envíos
En el modelo se encuentra normalmente con estos tipos de clases:
Facades: representan el punto de entrada principal a un dominio concreto dentro de la aplicación. Actúan como orquestador que coordina la colaboración entre distintos servicios para implementar casos de uso completos (como “crear un pedido” o “procesar un pago”). Bajo su capa de orquestación, la fachada oculta al resto de la aplicación los detalles de implementación y ofrece así una interfaz limpia para trabajar con ese dominio.
class OrderFacade
{
public function createOrder(Cart $cart): Order
{
// validación
// creación del pedido
// envío del correo
// escritura en las estadísticas
}
}
Servicios: se centran en operaciones de negocio concretas dentro de un dominio. A diferencia de las fachadas, que orquestan casos de uso completos, un servicio implementa una lógica de negocio concreta (como el cálculo de precios o el procesamiento de pagos). Los servicios normalmente no tienen estado y pueden usarlos las fachadas como bloques de construcción para operaciones más complejas, o bien directamente otras partes de la aplicación para tareas más simples.
class PricingService
{
public function calculateTotal(Order $order): Money
{
// cálculo del precio
}
}
Repositorios: se ocupan de toda la comunicación con el almacén de datos, típicamente una base de datos. Su tarea es cargar y guardar entidades e implementar métodos para buscarlas. El repositorio aísla al resto de la aplicación de los detalles de implementación de la base de datos y ofrece una interfaz orientada a objetos para trabajar con los datos.
class OrderRepository
{
public function find(int $id): ?Order
{
}
public function findByCustomer(int $customerId): array
{
}
}
Entidades: objetos que representan los principales conceptos de negocio de la aplicación, que tienen su propia identidad y cambian con el tiempo. Normalmente son clases mapeadas a tablas de la base de datos mediante un ORM (como Nette Database Explorer o Doctrine). Las entidades pueden contener reglas de negocio relativas a sus datos y lógica de validación.
// Entidad mapeada a la tabla 'orders' de la base de datos
class Order extends Nette\Database\Table\ActiveRow
{
public function addItem(Product $product, int $quantity): void
{
$this->related('order_items')->insert([
'product_id' => $product->id,
'quantity' => $quantity,
'unit_price' => $product->price,
]);
}
}
Value Objects: objetos inmutables que representan valores sin identidad propia, por ejemplo un importe monetario o una dirección de correo. Dos instancias de un value object con los mismos valores se consideran idénticas.
Código de infraestructura
La carpeta Core/ (o alternativamente Infrastructure/) alberga la base técnica de la aplicación. El
código de infraestructura incluye normalmente:
app/Core/ ├── Router/ ← enrutamiento y gestión de URL │ └── RouterFactory.php ├── Security/ ← autenticación y autorización │ ├── Authenticator.php │ └── Authorizator.php ├── Logging/ ← registro y monitorización │ ├── SentryLogger.php │ └── FileLogger.php ├── Cache/ ← capa de caché │ └── FullPageCache.php └── Integration/ ← integración con servicios externos ├── Slack/ └── Stripe/
Para proyectos más pequeños basta naturalmente con una estructura plana:
Core/ ├── RouterFactory.php ├── Authenticator.php └── QueueMailer.php
Es el código que:
- se ocupa de la infraestructura técnica (enrutamiento, registro, caché)
- integra servicios externos (Sentry, Elasticsearch, Redis)
- proporciona servicios básicos para toda la aplicación (correo, base de datos)
- es en su mayor parte independiente de un dominio concreto: la caché o el logger funcionan igual para una tienda online que para un blog.
¿Se pregunta si una determinada clase pertenece aquí o al modelo? La diferencia clave es que el código de
Core/:
- no sabe nada del dominio (productos, pedidos, artículos)
- normalmente se puede trasladar a otro proyecto
- resuelve el “cómo funciona” (cómo enviar un correo), no el “qué hace” (qué correo enviar)
Un ejemplo para entenderlo mejor:
App\Core\MailerFactorycrea instancias de la clase para enviar correos y se ocupa de la configuración SMTPApp\Model\OrderMailerusaMailerFactorypara enviar correos sobre pedidos, conoce sus plantillas y sabe cuándo deben enviarse
Scripts de comandos
Las aplicaciones necesitan a menudo realizar actividades fuera de las peticiones HTTP habituales, ya sea el procesamiento de
datos en segundo plano, el mantenimiento o tareas periódicas. Para ejecutarlas sirven scripts sencillos en el directorio
bin/, mientras que la lógica de implementación propiamente dicha se coloca en app/Tasks/ (o
app/Commands/).
Ejemplo:
app/Tasks/ ├── Maintenance/ ← scripts de mantenimiento │ ├── CleanupCommand.php ← borrado de datos antiguos │ └── DbOptimizeCommand.php ← optimización de la base de datos ├── Integration/ ← integración con sistemas externos │ ├── ImportProducts.php ← importación desde el sistema del proveedor │ └── SyncOrders.php ← sincronización de pedidos └── Scheduled/ ← tareas regulares ├── NewsletterCommand.php ← envío de newsletters └── ReminderCommand.php ← avisos a los clientes
¿Qué pertenece al modelo y qué a los scripts de comandos? Por ejemplo, la lógica del envío de un único correo forma parte
del modelo, mientras que el envío masivo de miles de correos pertenece a Tasks/.
Las tareas se ejecutan normalmente desde la línea de comandos o mediante cron: el script de bin/ crea el
contenedor DI con el método bootConsoleApplication() y saca de él el
servicio necesario. También se pueden ejecutar mediante una petición HTTP, pero hay que pensar en la seguridad. El presenter que
ejecuta la tarea necesita estar protegido, por ejemplo solo para usuarios conectados o con un token fuerte y acceso desde
direcciones IP permitidas. Para las tareas de larga duración es necesario aumentar el límite de tiempo del script y usar
session_write_close() para no bloquear la sesión.
Otros directorios posibles
Además de los directorios básicos mencionados puede añadir otras carpetas especializadas según las necesidades del proyecto. Veamos las más habituales y su uso:
app/ ├── Api/ ← lógica de la API independiente de la capa de presentación ├── Database/ ← scripts de migración y seeders para datos de prueba ├── Components/ ← componentes visuales compartidos por toda la aplicación ├── Event/ ← útil si se usa una arquitectura orientada a eventos ├── Mail/ ← plantillas de correo y la lógica relacionada └── Utils/ ← clases auxiliares
Para los componentes visuales compartidos que se usan en los presenters de toda la aplicación puede usar la carpeta
app/Components o app/Controls:
app/Components/ ├── Form/ ← componentes de formulario compartidos │ ├── SignInForm.php │ └── UserForm.php ├── Grid/ ← componentes para listados de datos │ └── DataGrid.php └── Navigation/ ← elementos de navegación ├── Breadcrumbs.php └── Menu.php
Aquí es donde pertenecen los componentes con una lógica más compleja. Si quiere compartir componentes entre varios proyectos, conviene extraerlos a un paquete de Composer separado.
En el directorio app/Mail puede colocar la gestión de la comunicación por correo:
app/Mail/ ├── templates/ ← plantillas de correo │ ├── order-confirmation.latte │ └── welcome.latte └── OrderMailer.php
Mapeo de presenters
El mapeo define las reglas para derivar el nombre de la clase a partir del nombre del presenter. Las indicamos en la configuración bajo la clave
application › mapping.
En esta página hemos mostrado que colocamos los presenters en la carpeta app/Presentation (o
app/UI). Desde Nette Application 3.3 esta es la convención predeterminada, que no hace falta configurar. Si usa una
estructura distinta o quiere indicar el mapeo explícitamente, la configuración predeterminada corresponde a esta línea:
application:
mapping: App\Presentation\*\**Presenter
¿Cómo funciona el mapeo? Para entenderlo mejor, imaginemos primero una aplicación sin módulos. Queremos que las clases de
los presenters caigan bajo el espacio de nombres App\Presentation, de modo que el presenter Home se
mapee a la clase App\Presentation\HomePresenter. Esto se consigue con esta configuración:
application:
mapping: App\Presentation\*Presenter
El mapeo funciona sustituyendo el asterisco de la máscara App\Presentation\*Presenter por el nombre del presenter
Home, lo que da como resultado el nombre final de la clase App\Presentation\HomePresenter.
¡Sencillo!
Sin embargo, como ve en los ejemplos de este y otros capítulos, colocamos las clases de los presenters en subdirectorios del
mismo nombre, por ejemplo el presenter Home se mapea a la clase App\Presentation\Home\HomePresenter.
Esto lo conseguimos usando un asterisco doble ** (requiere Nette Application 3.2.3):
application:
mapping: App\Presentation\**Presenter
Ahora pasamos al mapeo de presenters en módulos. Podemos definir un mapeo específico para cada módulo:
application:
mapping:
Front: App\Presentation\Front\**Presenter
Admin: App\Presentation\Admin\**Presenter
Api: App\Api\*Presenter
Según esta configuración, el presenter Front:Home se mapea a la clase
App\Presentation\Front\Home\HomePresenter, mientras que el presenter Api:OAuth se mapea a la clase
App\Api\OAuthPresenter.
Como los módulos Front y Admin tienen un patrón de mapeo parecido, y es probable que haya más
módulos así, es posible crear una regla general que los sustituya. A la máscara de la clase se le añade un nuevo asterisco
para el módulo:
application:
mapping:
*: App\Presentation\*\**Presenter
Api: App\Api\*Presenter
Funciona también para estructuras de directorios anidadas más profundamente, como el presenter Admin:User:Edit,
donde el segmento con el asterisco se repite para cada nivel de módulo, dando como resultado la clase
App\Presentation\Admin\User\Edit\EditPresenter.
Una notación alternativa consiste en usar, en lugar de una cadena, un array formado por tres segmentos. Para los ejemplos mostrados arriba, esta notación es equivalente a la anterior:
application:
mapping:
*: [App\Presentation, *, **Presenter]
Api: [App\Api, '', *Presenter]