Estructura de directorios de la aplicación
¿Cómo diseñar una estructura de directorios clara y escalable para proyectos en Nette Framework? Te mostraremos prácticas probadas que te ayudarán a organizar tu código. Aprenderás:
- cómo estructurar lógicamente la aplicación en directorios
- cómo diseñar la estructura para escalar bien a medida que crece el proyecto
- cuáles son las posibles alternativas y sus ventajas o desventajas
Es importante mencionar que Nette Framework no insiste en 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 por defecto probada en forma de Proyecto Web:
web-project/ ├── app/ ← directorio de la aplicación ├── assets/ ← SCSS, archivos JS, imágenes..., alternativamente resources/ ├── bin/ ← scripts de línea de comandos ├── config/ ← configuración ├── log/ ← errores registrados ├── temp/ ← archivos temporales, caché ├── tests/ ← pruebas ├── vendor/ ← bibliotecas instaladas por Composer └── www/ ← directorio público (document-root)
Puede modificar libremente esta estructura según sus necesidades – renombrar o mover carpetas. Entonces sólo tiene que
ajustar las rutas relativas a los directorios en Bootstrap.php
y posiblemente composer.json
. No se
necesita nada más, ni una reconfiguración compleja, ni cambios constantes. Nette tiene una autodetección inteligente y reconoce
automáticamente la ubicación de la aplicación incluyendo su URL base.
Principios de organización del código
Cuando exploras por primera vez un nuevo proyecto, deberías ser capaz de orientarte rápidamente. Imagina que haces clic en el
directorio app/Model/
y ves esta estructura:
app/Model/ ├── Services/ ├── Repositories/ └── Entities/
De esto, sólo aprenderás que el proyecto utiliza algunos servicios, repositorios y entidades. No aprenderás nada sobre el propósito real de la aplicación.
Veamos un enfoque diferente – organización por dominios:
app/Model/ ├── Cart/ ├── Payment/ ├── Order/ └── Product/
Esto es diferente – a primera vista está claro que se trata de un sitio de comercio electrónico. Los propios nombres de los directorios revelan lo que puede hacer la aplicación: trabaja con pagos, pedidos y productos.
El primer enfoque (organización por tipo de clase) trae varios problemas en la práctica: el código que está lógicamente relacionado está disperso en diferentes carpetas y hay que saltar entre ellas. Por lo tanto, organizaremos por dominios.
Espacios de nombres
Es convencional que la estructura de directorios se corresponda con los espacios de nombres de la aplicación. Esto significa
que la ubicación física de los archivos se corresponde con 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 orientar el código y simplifica la carga automática.
Singular y plural en los nombres
Observe que utilizamos el singular para los principales directorios de la aplicación: app
, config
,
log
, temp
, www
. Lo mismo ocurre dentro de la aplicación: Model
,
Core
, Presentation
. Esto se debe a que cada uno representa un concepto unificado.
Del mismo modo, 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 iphone.php
,
samsung.php
). Es un espacio de nombres que contiene clases para trabajar con productos –
ProductRepository.php
, ProductService.php
.
La carpeta app/Tasks
es plural porque contiene un conjunto de scripts ejecutables independientes –
CleanupTask.php
, ImportTask.php
. Cada uno de ellos es una unidad independiente.
Por coherencia, se recomienda utilizar:
- Singular para los espacios de nombres que representen una unidad funcional (aunque se trabaje con varias entidades).
- Plural para las colecciones de unidades independientes
- En caso de incertidumbre o si no quiere pensar en ello, elija singular
Directorio público www/
Este directorio es el único accesible desde la web (llamado document-root). A menudo puede encontrar el nombre
public/
en lugar de www/
– es sólo una cuestión de convención y no afecta a la funcionalidad. El
directorio contiene:
- Punto de entrada de la aplicación
index.php
- Archivo
.htaccess
con reglas mod_rewrite (para Apache) - Archivos estáticos (CSS, JavaScript, imágenes)
- Archivos subidos
Para una correcta seguridad de la aplicación, es crucial tener correctamente configurado el document-root.
Nunca coloques la carpeta node_modules/
en este directorio – contiene miles de archivos que pueden
ser ejecutables y no deberían ser accesibles públicamente.
Directorio de aplicaciones app/
Este es el directorio principal con el código de la aplicación. Estructura básica:
app/ ├── Core/ ← cuestiones de infraestructura ├── Model/ ← lógica empresarial ├── Presentation/ ← presentadores 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.
Veamos ahora en detalle los subdirectorios individuales.
Presentadores y plantillas
Tenemos la parte de presentación de la aplicación en el directorio app/Presentation
. Una alternativa es el corto
app/UI
. Este es el lugar para todos los presentadores, sus plantillas y cualquier clase de ayuda.
Organizamos esta capa por dominios. En un proyecto complejo que combine comercio electrónico, blog y API, la estructura tendría este aspecto:
app/Presentation/ ├── Shop/ ← frontend de comercio electrónico │ ├── Product/ │ ├── Cart/ │ └── Order/ ├── Blog/ ← blog │ ├── Home/ │ └── Post/ ├── Admin/ ← administración │ ├── Dashboard/ │ └── Products/ └── Api/ ← puntos finales API └── V1/
Por el contrario, para un blog sencillo utilizaríamos esta 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 presentadores y plantillas. Las carpetas como
Front/
, Admin/
o Api/
se denominan módulos. Técnicamente, se trata de directorios
normales que sirven para la organización lógica de la aplicación.
Cada carpeta con un presentador contiene un presentador de nombre similar y sus plantillas. Por ejemplo, la carpeta
Dashboard/
contiene:
Dashboard/ ├── DashboardPresenter.php ← presentador └── 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
(véase la asignación de presentadores):
namespace App\Presentation\Admin\Dashboard;
class DashboardPresenter extends Nette\Application\UI\Presenter
{
//...
}
Nos referimos al presentador Dashboard
dentro del módulo Admin
en la aplicación utilizando la
notación de dos puntos como Admin:Dashboard
. A su acción default
entonces como
Admin:Dashboard:default
. Para los módulos anidados, utilizamos más dos puntos, por ejemplo
Shop:Order:Detail:default
.
Desarrollo de estructuras flexibles
Una de las grandes ventajas de esta estructura es lo elegantemente que se adapta a las crecientes necesidades del proyecto. Como ejemplo, tomemos la parte de generación de feeds XML. Inicialmente, tenemos un simple formulario:
Export/ ├── ExportPresenter.php ← un presentador para todas las exportaciones ├── sitemap.latte ← plantilla para el mapa del sitio └── feed.latte ← plantilla para RSS feed
Con el tiempo, se añaden más 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 ├── amazon.latte ← feed para Amazon └── ebay.latte ← feed para eBay
Esta transformación es completamente fluida: basta con crear nuevas subcarpetas, dividir el código en ellas y actualizar los
enlaces (por ejemplo, de Export:feed
a Export:Feed:amazon
). Gracias a esto, podemos ampliar gradualmente
la estructura según sea necesario, el nivel de anidamiento no está limitado en modo alguno.
Por ejemplo, si en la administración tiene muchos presentadores relacionados con la gestión de pedidos, como
OrderDetail
, OrderEdit
, OrderDispatch
etc., puede crear un módulo (carpeta)
Order
para una mejor organización, que contendrá (carpetas para) los presentadores Detail
,
Edit
, Dispatch
y otros.
Ubicación de la plantilla
En los ejemplos anteriores, vimos que las plantillas se ubican directamente en la carpeta con el presentador:
Dashboard/ ├── DashboardPresenter.php ← presentador ├── DashboardTemplate.php ← clase de plantilla opcional └── default.latte ← plantilla
Esta ubicación resulta ser la más conveniente en la práctica – usted tiene todos los archivos relacionados a la mano.
Otra posibilidad es colocar las plantillas en una subcarpeta de templates/
. Nette admite ambas variantes. Incluso
puede colocar las plantillas completamente fuera de la carpeta Presentation/
. Encontrará toda la información sobre
las opciones de ubicación de las plantillas en el capítulo Búsqueda de plantillas.
Clases de ayuda y componentes
Los presentadores y las plantillas suelen venir acompañados de otros archivos de ayuda. Los colocamos lógicamente según su ámbito de aplicación:
1. Directamente con el presentador en caso de componentes específicos para el presentador dado:
Product/ ├── ProductPresenter.php ├── ProductGrid.php ← componente para el listado de productos └── FilterForm.php ← formulario de filtrado
2. Para el módulo – se recomienda utilizar la carpeta Accessory
, que se coloca ordenadamente al
principio del alfabeto:
Front/ ├── Accessory/ │ ├── NavbarControl.php ← componentes para 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 de ayuda 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.
Modelo – 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 la misma regla – estructuramos por dominios:
app/Model/ ├── Payment/ ← todo sobre los 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, normalmente se encuentran estos tipos de clases:
Facades: representan el principal punto de entrada a un dominio específico en la aplicación. Actúan como un orquestador que coordina la cooperación entre diferentes servicios para 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 al resto de la aplicación, proporcionando así una interfaz limpia para trabajar con el dominio en cuestión.
class OrderFacade
{
public function createOrder(Cart $cart): Order
{
// validación
// creación de pedidos
// envío de correos electrónicos
// estadísticas
}
}
Servicios: se centran en operaciones de negocio específicas dentro de un dominio. A diferencia de las fachadas, que orquestan casos de uso completos, un servicio implementa una lógica de negocio específica (como el cálculo de precios o el procesamiento de pagos). Los servicios suelen carecer de estado y pueden ser utilizados por las fachadas como bloques de construcción para operaciones más complejas, o directamente por otras partes de la aplicación para tareas más sencillas.
class PricingService
{
public function calculateTotal(Order $order): Money
{
// cálculo del precio
}
}
Repositorios: gestionan toda la comunicación con el almacenamiento de datos, normalmente una base de datos. Su tarea es cargar y guardar entidades e implementar métodos para buscarlas. Un repositorio protege 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 de la aplicación, que tienen su identidad y cambian con el tiempo. Suelen ser clases asignadas a tablas de bases de datos mediante 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 asignada a la tabla de base de datos de pedidos
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 valor: objetos inmutables que representan valores sin identidad propia – por ejemplo, una cantidad de dinero o una dirección de correo electrónico. Dos instancias de un objeto valor con los mismos valores se consideran idénticas.
Código de infraestructura
La carpeta Core/
(o también Infrastructure/
) alberga la base técnica de la aplicación. El código
de infraestructura suele incluir:
app/Core/ ├── Router/ ← enrutamiento y gestión de URL │ └── RouterFactory.php ├── Security/ ← autenticación y autorización │ ├── Authenticator.php │ └── Authorizator.php ├── Logging/ ← registro y supervisió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, una estructura plana es naturalmente suficiente:
Core/ ├── RouterFactory.php ├── Authenticator.php └── QueueMailer.php
Esto es código que:
- Gestiona la infraestructura técnica (enrutamiento, registro, almacenamiento en 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 del dominio específico – la caché o el logger funcionan igual para el comercio electrónico o el blog.
¿Te preguntas si una determinada clase debe estar aquí o en el modelo? La diferencia clave es que el código en
Core/
:
- No sabe nada sobre el dominio (productos, pedidos, artículos)
- Normalmente puede transferirse a otro proyecto
- Resuelve “cómo funciona” (cómo enviar correo), no “qué hace” (qué correo enviar)
Ejemplo para una mejor comprensión:
App\Core\MailerFactory
– crea instancias de la clase de envío de correo electrónico, gestiona la configuración SMTPApp\Model\OrderMailer
– utilizaMailerFactory
para enviar correos sobre pedidos, conoce sus plantillas y cuándo deben enviarse
Scripts de comandos
Las aplicaciones a menudo necesitan realizar tareas fuera de las peticiones HTTP habituales, ya sea procesamiento de datos en
segundo plano, mantenimiento o tareas periódicas. Los scripts simples en el directorio bin/
se utilizan para la
ejecución, mientras que la lógica de implementación real se coloca 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 boletines └── ReminderCommand.php ← notificaciones a clientes
¿Qué pertenece al modelo y qué a las secuencias de comandos? Por ejemplo, la lógica para enviar un correo electrónico
forma parte del modelo, el envío masivo de miles de correos electrónicos pertenece a Tasks/
.
Las tareas suelen ejecutarse desde la línea de
comandos o mediante cron. También pueden ejecutarse a través de una petición HTTP, pero hay que tener en cuenta la
seguridad. El presentador que ejecuta la tarea debe estar protegido, por ejemplo, sólo para usuarios registrados o con un token
seguro y acceso desde direcciones IP permitidas. Para tareas largas, es necesario aumentar el límite de tiempo del script y
utilizar session_write_close()
para evitar el bloqueo de la sesión.
Otros directorios posibles
Además de los directorios básicos mencionados, puedes añadir otras carpetas especializadas según las necesidades del proyecto. Veamos las más comunes y su uso:
app/ ├── Api/ ← lógica de la API independiente de la capa de presentación ├── Database/ ← scripts de migración y sembradores de datos de prueba ├── Components/ ← componentes visuales compartidos en toda la aplicación ├── Event/ ← útil si se utiliza una arquitectura basada en eventos ├── Mail/ ← plantillas de correo electrónico y lógica relacionada └── Utils/ ← clases de ayuda
Para los componentes visuales compartidos que se utilizan en los presentadores de toda la aplicación, puedes utilizar 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 quieres compartir componentes entre varios proyectos, es bueno separarlos en un paquete compositor independiente.
En el directorio app/Mail
puedes 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
Mapa del presentador
El mapeo define reglas para derivar nombres de clase a partir de nombres de presentador. Las especificamos en la configuración bajo la clave
application › mapping
.
En esta página, hemos mostrado que colocamos los presentadores en la carpeta app/Presentation
(o
app/UI
). Debemos informar a Nette de esta convención en el archivo de configuración. Una línea es suficiente:
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 del
presentador pertenezcan al espacio de nombres App\Presentation
, de modo que el presentador Home
se
asigne 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
presentador Home
, dando como resultado final el nombre de la clase App\Presentation\HomePresenter
. Muy
sencillo.
Sin embargo, como puede ver en los ejemplos de este y otros capítulos, colocamos las clases presentadoras en subdirectorios
epónimos, por ejemplo, el presentador Home
se asigna a la clase App\Presentation\Home\HomePresenter
.
Esto se consigue duplicando los dos puntos (requiere Nette Application 3.2):
application:
mapping: App\Presentation\**Presenter
Ahora pasaremos a mapear presentadores en módulos. Podemos definir una asignación específica 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 presentador Front:Home
se asigna a la clase
App\Presentation\Front\Home\HomePresenter
, mientras que el presentador Api:OAuth
se asigna a la clase
App\Api\OAuthPresenter
.
Dado que los módulos Front
y Admin
tienen un método de asignación similar y que probablemente
habrá más módulos de este tipo, es posible crear una regla general que los sustituya. Se añadirá un nuevo asterisco para el
módulo a la máscara de clase:
application:
mapping:
*: App\Presentation\*\**Presenter
Api: App\Api\*Presenter
También funciona para estructuras de directorios anidadas más profundas, como el presentador Admin:User:Edit
,
donde el segmento con asterisco se repite para cada nivel y da como resultado la clase
App\Presentation\Admin\User\Edit\EditPresenter
.
Una notación alternativa consiste en utilizar una matriz formada 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]