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 SMTP
  • App\Model\OrderMailer – usa MailerFactory 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]
versión: 4.0