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 .htaccess con 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\MailerFactory crea instancias de la clase para enviar correos y se ocupa de la configuración SMTP
  • App\Model\OrderMailer usa MailerFactory para 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]
versión: 4.x