Estructura de directorios de la aplicación
¿Cómo diseñar una estructura de directorios clara y escalable para proyectos en Nette Framework? Mostraremos las mejores prácticas que le ayudarán a organizar su código. Aprenderá:
- cómo dividir lógicamente la aplicación en directorios
- cómo diseñar la estructura para que escale bien con el crecimiento del proyecto
- cuáles son las alternativas posibles y sus ventajas o desventajas
Es importante mencionar que Nette Framework en sí mismo no impone ninguna estructura específica. 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 de Web Project:
web-project/ ├── app/ ← directorio con 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/ ← pruebas ├── vendor/ ← librerías 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 necesita
actualizar las rutas relativas a los directorios en el archivo Bootstrap.php
y, opcionalmente, en
composer.json
. No se necesita 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 base
de URL.
Principios de organización del código
Cuando explora un nuevo proyecto por primera vez, debería poder orientarse rápidamente en él. Imagine que expande el
directorio app/Model/
y ve esta estructura:
app/Model/ ├── Services/ ├── Repositories/ └── Entities/
De ella, solo deduce que el proyecto utiliza algunos servicios, repositorios y entidades. No aprenderá nada sobre el propósito real de la aplicación.
Veamos otro enfoque: organización por dominios:
app/Model/ ├── Cart/ ├── Payment/ ├── Order/ └── Product/
Aquí es diferente: a primera vista, está claro que se trata de una tienda electrónica. Los propios nombres de los directorios revelan lo que hace la aplicación: trabaja con pagos, pedidos y productos.
El primer enfoque (organización por tipo de clase) presenta una serie de problemas en la práctica: el código que está lógicamente relacionado está disperso en diferentes carpetas y tiene que saltar entre ellas. Por lo tanto, organizaremos por dominios.
Espacios de nombres
Es costumbre que la estructura de directorios corresponda a los espacios de nombres en la aplicación. Esto significa que la
ubicación física de los archivos corresponde a su espacio de nombres. Por ejemplo, una clase ubicada 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 autocarga.
Singular vs plural en los nombres
Observe que para los directorios principales de la aplicación usamos el singular: app
, config
,
log
, temp
, www
. Lo mismo dentro de la aplicación: Model
, Core
,
Presentation
. Esto se debe a que cada uno de ellos representa un concepto coherente.
De manera similar, por ejemplo, app/Model/Product
representa todo lo relacionado con los productos. No lo
llamaremos Products
, porque no es una carpeta llena de productos (eso significaría que habría archivos
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 independiente.
Para mantener la coherencia, recomendamos usar:
- Singular para espacios de nombres que representan una unidad funcional (aunque trabajen con múltiples entidades)
- Plural para colecciones de unidades independientes
- En caso de duda o si no quiere pensar en ello, elija el singular
Directorio público www/
Este directorio es el único accesible desde la web (el llamado document-root). A menudo también puede encontrar el nombre
public/
en lugar de www/
: es solo una cuestión de convención y no afecta la funcionalidad del
framework. El directorio contiene:
- El punto de entrada de la aplicación
index.php
- El archivo
.htaccess
con reglas para mod_rewrite (para Apache) - Archivos estáticos (CSS, JavaScript, imágenes)
- Archivos subidos
Para la seguridad adecuada de la aplicación, es crucial tener el document-root correctamente configurado.
Nunca coloque la carpeta node_modules/
en este directorio: contiene miles de archivos que pueden ser
ejecutables y no deben ser accesibles públicamente.
Directorio de aplicación app/
Este es el directorio principal con 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 inicio de la aplicación
que inicializa el entorno, carga la configuración y crea el contenedor DI.
Ahora veamos los subdirectorios individuales con más detalle.
Presenters y plantillas
La parte de presentación de la aplicación la tenemos en el directorio app/Presentation
. Una alternativa es el
corto app/UI
. Es el lugar para todos los presenters, sus plantillas y posibles clases auxiliares.
Organizamos esta capa por dominios. En un proyecto complejo que combina una tienda electrónica, un blog y una API, la estructura se vería así:
app/Presentation/ ├── Shop/ ← frontend de la tienda electrónica │ ├── Product/ │ ├── Cart/ │ └── Order/ ├── Blog/ ← blog │ ├── Home/ │ └── Post/ ├── Admin/ ← administración │ ├── Dashboard/ │ └── Products/ └── Api/ ← endpoints de la API └── V1/
Por el contrario, para un blog simple, usaríamos la siguiente división:
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/
las llamamos módulos. Técnicamente, son directorios normales
que sirven para la división lógica de la aplicación.
Cada carpeta con un presenter contiene un presenter con el mismo nombre 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
se encuentra en el espacio de nombres App\Presentation\Admin\Dashboard
(ver mapování presenterů):
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 usando la notación
de dos puntos como Admin:Dashboard
. A su acción default
entonces como
Admin:Dashboard:default
. En caso de módulos anidados, usamos más dos puntos, por ejemplo
Shop:Order:Detail:default
.
Desarrollo flexible de la estructura
Una de las grandes ventajas de esta estructura es cómo se adapta elegantemente a las crecientes necesidades del proyecto. Como ejemplo, tomemos la parte que genera feeds XML. Al principio, tenemos una forma simple:
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 agregan otros tipos de feeds y necesitamos más lógica para ellos… ¡No hay problema! La carpeta
Export/
simplemente se convierte en un módulo:
Export/ ├── Sitemap/ │ ├── SitemapPresenter.php │ └── sitemap.latte └── Feed/ ├── FeedPresenter.php ├── zbozi.latte ← feed para Zboží.cz └── heureka.latte ← feed para Heureka.cz
Esta transformación es completamente fluida: basta con crear nuevas subcarpetas, dividir el código en ellas y actualizar los
enlaces (p. ej., de Export:feed
a Export:Feed:zbozi
). Gracias a esto, podemos expandir gradualmente la
estructura según sea necesario, el nivel de anidamiento no está limitado de ninguna manera.
Si, por ejemplo, en la administración tiene muchos presenters relacionados con la gestión de pedidos, como
OrderDetail
, OrderEdit
, OrderDispatch
, etc., puede crear un módulo (carpeta)
Order
en este lugar para una mejor organización, que contendrá (carpetas para) los presenters Detail
,
Edit
, Dispatch
y otros.
Ubicación de las plantillas
En los ejemplos anteriores, vimos que las plantillas se ubican directamente en la carpeta con el presenter:
Dashboard/ ├── DashboardPresenter.php ← presenter ├── DashboardTemplate.php ← clase opcional para la plantilla └── default.latte ← plantilla
Esta ubicación resulta ser la más conveniente 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 plantillas se encuentra en el capítulo Búsqueda
de plantillas.
Clases auxiliares y componentes
A los presenters y plantillas a menudo les pertenecen otros archivos auxiliares. Los ubicamos lógicamente según su ámbito:
1. Directamente junto al presenter en caso de componentes específicos para ese presenter:
Product/ ├── ProductPresenter.php ├── ProductGrid.php ← componente para listar productos └── FilterForm.php ← formulario para filtrar
2. Para el módulo – recomendamos usar la carpeta Accessory
, que se coloca convenientemente al
principio del alfabeto:
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/
O puede colocar 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
costumbres del equipo.
Modelo – el corazón de la aplicación
El modelo contiene toda la lógica de negocio de la aplicación. Para su organización, se aplica nuevamente la regla: estructuramos por dominios:
app/Model/ ├── Payment/ ← todo sobre pagos │ ├── PaymentFacade.php ← punto de entrada principal │ ├── PaymentRepository.php │ ├── Payment.php ← entidad ├── Order/ ← todo sobre pedidos │ ├── OrderFacade.php │ ├── OrderRepository.php │ ├── Order.php └── Shipping/ ← todo sobre envíos
En el modelo, típicamente encontrará estos tipos de clases:
Fachadas (Facades): representan el punto de entrada principal a un dominio específico en la aplicación. Actúan como un orquestador que coordina la colaboración entre diferentes servicios con el fin de implementar casos de uso completos (como “crear pedido” o “procesar pago”). Bajo su capa de orquestación, la fachada oculta los detalles de implementación del resto de la aplicación, proporcionando así una interfaz limpia para trabajar con el dominio dado.
class OrderFacade
{
public function createOrder(Cart $cart): Order
{
// validación
// creación del pedido
// envío de correo electrónico
// registro en estadísticas
}
}
Servicios: se centran en una operación de negocio específica dentro del dominio. A diferencia de la fachada, que orquesta casos de uso completos, el servicio implementa lógica de negocio específica (como cálculos de precios o procesamiento de pagos). Los servicios suelen ser sin estado y pueden ser utilizados ya sea por fachadas como bloques de construcción para operaciones más complejas, o directamente por otras partes de la aplicación para tareas más simples.
class PricingService
{
public function calculateTotal(Order $order): Money
{
// cálculo del precio
}
}
Repositorios: aseguran toda la comunicación con el almacenamiento de datos, típicamente una base de datos. Su tarea es cargar y guardar entidades e implementar métodos para su búsqueda. El repositorio aísla al resto de la aplicación de los detalles de implementación de la base de datos y proporciona 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 en la aplicación, que tienen su identidad y cambian con el tiempo. Típicamente, son clases mapeadas a tablas de bases de datos usando un ORM (como Nette Database Explorer o Doctrine). Las entidades pueden contener reglas de negocio relacionadas con sus datos y lógica de validación.
// Entidad mapeada a la tabla de base de datos orders
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,
]);
}
}
Objetos de valor (Value objects): objetos inmutables que representan valores sin identidad propia – por ejemplo, una cantidad monetaria o una dirección de correo electrónico. Dos instancias de un objeto de valor con los mismos valores son consideradas idénticas.
Código de infraestructura
La carpeta Core/
(o también Infrastructure/
) es el hogar de la base técnica de la aplicación. El
código de infraestructura típicamente incluye:
app/Core/ ├── Router/ ← enrutamiento y gestión de URL │ └── RouterFactory.php ├── Security/ ← autenticación y autorización │ ├── Authenticator.php │ └── Authorizator.php ├── Logging/ ← registro y monitoreo │ ├── SentryLogger.php │ └── FileLogger.php ├── Cache/ ← capa de caché │ └── FullPageCache.php └── Integration/ ← integración con servicios ext. ├── Slack/ └── Stripe/
En proyectos más pequeños, por supuesto, basta con una estructura plana:
Core/ ├── RouterFactory.php ├── Authenticator.php └── QueueMailer.php
Se trata de código que:
- Resuelve 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 mayormente independiente del dominio específico – la caché o el logger funciona igual para una tienda electrónica o un blog.
¿Duda si una clase determinada pertenece aquí o al modelo? La diferencia clave es que el código en Core/
:
- No sabe nada sobre el dominio (productos, pedidos, artículos)
- Es mayormente posible transferirlo a otro proyecto
- Resuelve “cómo funciona” (cómo enviar un correo), no “qué hace” (qué correo enviar)
Ejemplo para una mejor comprensión:
App\Core\MailerFactory
– crea instancias de la clase para enviar correos electrónicos, resuelve la configuración SMTPApp\Model\OrderMailer
– usaMailerFactory
para enviar correos electrónicos sobre pedidos, conoce sus plantillas y sabe cuándo deben enviarse
Scripts de comandos
Las aplicaciones a menudo necesitan realizar actividades fuera de las peticiones HTTP normales – ya sea procesamiento de
datos en segundo plano, mantenimiento o tareas periódicas. Para la ejecución sirven scripts simples en el directorio
bin/
, la lógica de implementación la colocamos en app/Tasks/
(o app/Commands/
).
Ejemplo:
app/Tasks/ ├── Maintenance/ ← scripts de mantenimiento │ ├── CleanupCommand.php ← eliminación 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 periódicas ├── NewsletterCommand.php ← envío de newsletters └── ReminderCommand.php ← notificaciones a clientes
¿Qué pertenece al modelo y qué a los scripts de comandos? Por ejemplo, la lógica para enviar un correo electrónico es
parte del modelo, el envío masivo de miles de correos electrónicos ya pertenece a Tasks/
.
Las tareas generalmente se ejecutan desde la línea de
comandos o a través de cron. También se pueden ejecutar a través de una petición HTTP, pero es necesario pensar en la
seguridad. El presenter que ejecuta la tarea debe estar protegido, por ejemplo, solo para usuarios autenticados o con un token
fuerte y acceso desde direcciones IP permitidas. Para tareas largas, es necesario aumentar el límite de tiempo del script y usar
session_write_close()
para que la sesión no se bloquee.
Otros directorios posibles
Además de los directorios básicos mencionados, puede agregar otras carpetas especializadas según las necesidades del proyecto. Veamos las más comunes y su uso:
app/ ├── Api/ ← lógica para la API independiente de la capa de presentación ├── Database/ ← scripts de migración y seeders para datos de prueba ├── Components/ ← componentes visuales compartidos en toda la aplicación ├── Event/ ← útil si usa arquitectura dirigida por eventos ├── Mail/ ← plantillas de correo electrónico y lógica relacionada └── Utils/ ← clases auxiliares
Para componentes visuales compartidos utilizados en presenters en toda la aplicación, se 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í pertenecen los componentes que tienen una lógica más compleja. Si desea compartir componentes entre varios proyectos, es recomendable extraerlos a un paquete composer separado.
En el directorio app/Mail
puede colocar la gestión de la comunicación por correo electrónico:
app/Mail/ ├── templates/ ← plantillas de correo electrónico │ ├── order-confirmation.latte │ └── welcome.latte └── OrderMailer.php
Mapeo de presenters
El mapeo define reglas para derivar el nombre de la clase a partir del nombre del presenter. Las especificamos 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
). Debemos comunicar esta convención a Nette en el archivo de configuración. Basta con una línea:
application:
mapping: App\Presentation\*\**Presenter
¿Cómo funciona el mapeo? Para una mejor comprensión, imaginemos primero una aplicación sin módulos. Queremos que las
clases de los presenters caigan en el espacio de nombres App\Presentation
, para que el presenter Home
se
mapee a la clase App\Presentation\HomePresenter
. Lo cual logramos con esta configuración:
application:
mapping: App\Presentation\*Presenter
El mapeo funciona de tal manera que el nombre del presenter Home
reemplaza el asterisco en la máscara
App\Presentation\*Presenter
, obteniendo así el nombre de clase resultante
App\Presentation\HomePresenter
. ¡Simple!
Pero como puede ver en los ejemplos de este y otros capítulos, colocamos las clases de los presenters en subdirectorios
epónimos, por ejemplo, el presenter Home
se mapea a la clase App\Presentation\Home\HomePresenter
. Esto
se logra duplicando los dos puntos (requiere Nette Application 3.2):
application:
mapping: App\Presentation\**Presenter
Ahora procederemos a mapear los presenters a módulos. Para cada módulo podemos definir un mapeo específico:
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
a la clase
App\Api\OAuthPresenter
.
Dado que los módulos Front
y Admin
tienen una forma similar de mapeo y probablemente habrá más
módulos de este tipo, es posible crear una regla general que los reemplace. Así, se agregará un nuevo asterisco a la máscara
de clase para el módulo:
application:
mapping:
*: App\Presentation\*\**Presenter
Api: App\Api\*Presenter
Funciona también para estructuras de directorios más profundamente anidadas, como por ejemplo el presenter
Admin:User:Edit
, el segmento con asterisco se repite para cada nivel y el resultado es la clase
App\Presentation\Admin\User\Edit\EditPresenter
.
Una notación alternativa es usar un array compuesto por tres segmentos en lugar de una cadena. Esta notación es equivalente a la anterior:
application:
mapping:
*: [App\Presentation, *, **Presenter]
Api: [App\Api, '', *Presenter]