Struttura delle directory dell'applicazione

Come progettare una struttura di directory chiara e scalabile per i progetti in Nette Framework? Vi mostreremo pratiche collaudate che vi aiuteranno a organizzare il codice. Imparerete:

  • come strutturare logicamente l'applicazione in directory
  • come progettare la struttura perché scali bene con la crescita del progetto
  • quali sono le alternative possibili e i loro pregi o difetti

È importante dire che Nette Framework stesso non impone alcuna struttura specifica. È progettato per adattarsi facilmente a qualsiasi esigenza e preferenza.

Struttura di base del progetto

Benché Nette Framework non imponga alcuna struttura fissa di directory, esiste una disposizione predefinita collaudata, sotto forma di Web Project:

web-project/
├── app/              ← directory dell'applicazione
├── assets/           ← file SCSS, JS, immagini..., in alternativa resources/
├── bin/              ← script per la riga di comando
├── config/           ← configurazione
├── log/              ← errori registrati
├── temp/             ← file temporanei, cache
├── tests/            ← test
├── vendor/           ← librerie installate da Composer
└── www/              ← directory pubblica (document-root)

Potete modificare liberamente questa struttura secondo le vostre esigenze, rinominando o spostando le cartelle. Dovete poi solo sistemare i percorsi relativi delle directory in Bootstrap.php ed eventualmente in composer.json. Non serve nient'altro: nessuna riconfigurazione complicata, nessuna modifica alle costanti. Nette dispone di un rilevamento automatico intelligente e riconosce da sé la posizione dell'applicazione, compresa la base del suo URL.

Principi di organizzazione del codice

Quando esplorate per la prima volta un nuovo progetto, dovreste riuscire a orientarvi rapidamente. Immaginate di cliccare sulla directory app/Model/ e di vedere questa struttura:

app/Model/
├── Services/
├── Repositories/
└── Entities/

Da qui imparate solo che il progetto usa dei servizi, dei repository e delle entità. Non imparate nulla sullo scopo reale dell'applicazione.

Guardiamo un approccio diverso, l'organizzazione per domini:

app/Model/
├── Cart/
├── Payment/
├── Order/
└── Product/

Qui è diverso: a colpo d'occhio è chiaro che si tratta di un e-shop. I nomi stessi delle directory rivelano cosa sa fare l'applicazione: lavora con pagamenti, ordini e prodotti.

Il primo approccio (l'organizzazione per tipo di classe) porta nella pratica diversi problemi: il codice logicamente collegato è frammentato in cartelle diverse e dovete saltare tra di esse. Organizzeremo quindi per domini.

Namespace

È consuetudine che la struttura delle directory corrisponda ai namespace dell'applicazione. Questo significa che la posizione fisica dei file coincide con il loro namespace. Per esempio, una classe che si trova in app/Model/Product/ProductRepository.php dovrebbe avere il namespace App\Model\Product. Questo principio aiuta a orientarsi nel codice e semplifica l'autoloading.

Singolare o plurale nei nomi

Notate che per le directory principali dell'applicazione usiamo il singolare: app, config, log, temp, www. Lo stesso vale dentro l'applicazione: Model, Core, Presentation. Questo perché ciascuna rappresenta un unico concetto coerente.

Allo stesso modo app/Model/Product rappresenta tutto ciò che riguarda i prodotti. Non la chiamiamo Products perché non è una cartella piena di prodotti (conterrebbe file come nokia.php, samsung.php). È un namespace che contiene le classi per lavorare con i prodotti: ProductRepository.php, ProductService.php.

La cartella app/Tasks è al plurale perché contiene un insieme di script eseguibili separati: CleanupTask.php, ImportTask.php. Ognuno di essi è un'unità indipendente.

Per coerenza consigliamo di usare:

  • il singolare per i namespace che rappresentano un'unità funzionale (anche se lavorano con più entità)
  • il plurale per le raccolte di unità indipendenti
  • in caso di incertezza, o se non volete pensarci, scegliete il singolare

Directory pubblica www/

Questa directory è l'unica accessibile dal web (il document-root). Spesso potreste incontrare il nome public/ al posto di www/: è solo una questione di convenzione e non influisce sul funzionamento dell'applicazione. La directory contiene:

  • il punto d'ingresso dell'applicazione index.php
  • il file .htaccess con le regole di mod_rewrite (per Apache)
  • i file statici (CSS, JavaScript, immagini)
  • i file caricati

Per una corretta sicurezza dell'applicazione è essenziale avere il document-root configurato correttamente.

Non collocate mai la cartella node_modules/ in questa directory: contiene migliaia di file che potrebbero essere eseguibili e non dovrebbero essere accessibili pubblicamente.

Directory dell'applicazione app/

È la directory principale, che contiene il codice dell'applicazione. Struttura di base:

app/
├── Core/               ← questioni infrastrutturali
├── Model/              ← logica di business
├── Presentation/       ← presenter e template
├── Tasks/              ← script da riga di comando
└── Bootstrap.php       ← classe di avvio dell'applicazione

Bootstrap.php è la classe di avvio dell'applicazione, che inizializza l'ambiente, carica la configurazione e crea il container DI.

Vediamo ora più in dettaglio le singole sottodirectory.

Presenter e template

La parte di presentazione dell'applicazione si trova nella directory app/Presentation. Un'alternativa è la più breve app/UI. È il posto di tutti i presenter, dei loro template e delle eventuali classi di supporto collegate.

Organizziamo questo strato per domini. In un progetto complesso che unisce un e-shop, un blog e un'API, la struttura avrebbe questo aspetto:

app/Presentation/
├── Shop/              ← frontend dell'e-shop
│   ├── Product/
│   ├── Cart/
│   └── Order/
├── Blog/              ← blog
│   ├── Home/
│   └── Post/
├── Admin/             ← amministrazione
│   ├── Dashboard/
│   └── Products/
└── Api/               ← endpoint dell'API
	└── V1/

Al contrario, per un semplice blog useremmo questa struttura:

app/Presentation/
├── Front/             ← frontend del sito
│   ├── Home/
│   └── Post/
├── Admin/             ← amministrazione
│   ├── Dashboard/
│   └── Posts/
├── Error/
└── Export/            ← RSS, sitemap ecc.

Cartelle come Home/ o Dashboard/ contengono presenter e template. Cartelle come Front/, Admin/ o Api/ si chiamano moduli. Tecnicamente sono normali directory usate per la suddivisione logica dell'applicazione.

Ogni cartella che contiene un presenter comprende il file del presenter stesso e i suoi template. Per esempio la cartella Dashboard/ contiene:

Dashboard/
├── DashboardPresenter.php     ← presenter
└── default.latte              ← template

Questa struttura di directory si riflette nei namespace delle classi. Per esempio DashboardPresenter si trova nel namespace App\Presentation\Admin\Dashboard (vedi Mapping dei presenter):

namespace App\Presentation\Admin\Dashboard;

class DashboardPresenter extends Nette\Application\UI\Presenter
{
	// ...
}

Nell'applicazione ci riferiamo al presenter Dashboard del modulo Admin con la notazione a due punti, come Admin:Dashboard. La sua azione default si indica poi come Admin:Dashboard:default. Per i moduli annidati usiamo più volte i due punti, per esempio Shop:Order:Detail:default.

Sviluppo flessibile della struttura

Uno dei grandi vantaggi di questa struttura è la sua eleganza nell'adattarsi alle esigenze crescenti del progetto. Prendiamo come esempio la parte che genera i feed XML. All'inizio abbiamo una forma semplice:

Export/
├── ExportPresenter.php   ← un presenter per tutte le esportazioni
├── sitemap.latte         ← template della sitemap
└── feed.latte            ← template del feed RSS

Col tempo si aggiungono altri tipi di feed e ci serve più logica per gestirli… Nessun problema! La cartella Export/ diventa semplicemente un modulo:

Export/
├── Sitemap/
│   ├── SitemapPresenter.php
│   └── sitemap.latte
└── Feed/
	├── FeedPresenter.php
	├── amazon.latte         ← feed per Amazon
	└── ebay.latte           ← feed per eBay

Questa trasformazione è del tutto indolore: basta creare le nuove sottocartelle, dividervi il codice e aggiornare i link (per esempio da Export:feed a Export:Feed:amazon). Grazie a questo possiamo ampliare gradualmente la struttura secondo necessità, e il livello di annidamento non è limitato in alcun modo.

Se per esempio nell'amministrazione avete molti presenter legati alla gestione degli ordini, come OrderDetail, OrderEdit, OrderDispatch ecc., per una migliore organizzazione potete creare un modulo (una cartella) chiamato Order, che conterrà (le cartelle dei) presenter Detail, Edit, Dispatch e altri.

Posizione dei template

Negli esempi precedenti abbiamo visto che i template si trovano direttamente nella cartella del presenter:

Dashboard/
├── DashboardPresenter.php     ← presenter
├── DashboardTemplate.php      ← classe del template, facoltativa
└── default.latte              ← template

Nella pratica questa collocazione si rivela la più comoda: avete tutti i file collegati subito a portata di mano.

In alternativa potete collocare i template in una sottocartella templates/. Nette supporta entrambe le varianti. Potete perfino collocare i template completamente fuori dalla cartella Presentation/. Tutto ciò che riguarda le possibilità di collocazione dei template si trova nel capitolo Ricerca dei template.

Classi di supporto e componenti

Ai presenter e ai template si accompagnano spesso altri file di supporto. Li collochiamo logicamente in base al loro ambito:

1. Direttamente con il presenter, nel caso di componenti specifici di quel presenter:

Product/
├── ProductPresenter.php
├── ProductGrid.php        ← componente per l'elenco dei prodotti
└── FilterForm.php         ← form per il filtraggio

2. Per il modulo: consigliamo di usare la cartella Accessory, che in ordine alfabetico si colloca comodamente all'inizio:

Front/
├── Accessory/
│   ├── NavbarControl.php    ← componenti per il frontend
│   └── TemplateFilters.php
├── Product/
└── Cart/

3. Per l'intera applicazione: in Presentation/Accessory/:

app/Presentation/
├── Accessory/
│   ├── LatteExtension.php
│   └── TemplateFilters.php
├── Front/
└── Admin/

In alternativa potete collocare le classi di supporto come LatteExtension.php o TemplateFilters.php nella cartella infrastrutturale app/Core/Latte/. E i componenti in app/Components. La scelta dipende dalle convenzioni del team.

Model, il cuore dell'applicazione

Il model contiene tutta la logica di business dell'applicazione. La regola per organizzarlo è di nuovo: struttura per domini.

app/Model/
├── Payment/                   ← tutto ciò che riguarda i pagamenti
│   ├── PaymentFacade.php      ← punto d'ingresso principale
│   ├── PaymentRepository.php
│   ├── Payment.php            ← entità
├── Order/                     ← tutto ciò che riguarda gli ordini
│   ├── OrderFacade.php
│   ├── OrderRepository.php
│   ├── Order.php
└── Shipping/                  ← tutto ciò che riguarda le spedizioni

Nel model incontrate di norma questi tipi di classi:

Facade: rappresentano il punto d'ingresso principale in un determinato dominio dell'applicazione. Fanno da orchestratore e coordinano la collaborazione tra i vari servizi per realizzare interi casi d'uso (come “crea ordine” o “elabora pagamento”). Sotto il proprio strato di orchestrazione la facade nasconde i dettagli implementativi al resto dell'applicazione, offrendo così un'interfaccia pulita per lavorare con quel dominio.

class OrderFacade
{
	public function createOrder(Cart $cart): Order
	{
		// validazione
		// creazione dell'ordine
		// invio dell'e-mail
		// scrittura nelle statistiche
	}
}

Servizi: si concentrano su operazioni di business specifiche all'interno di un dominio. A differenza delle facade, che orchestrano interi casi d'uso, un servizio implementa una logica di business precisa (come il calcolo dei prezzi o l'elaborazione dei pagamenti). I servizi sono di norma privi di stato e possono essere usati sia dalle facade come mattoni di operazioni più complesse, sia direttamente da altre parti dell'applicazione per compiti più semplici.

class PricingService
{
	public function calculateTotal(Order $order): Money
	{
		// calcolo del prezzo
	}
}

Repository: gestiscono tutta la comunicazione con l'archivio dei dati, di norma un database. Il loro compito è caricare e salvare le entità e implementare i metodi per cercarle. Un repository protegge il resto dell'applicazione dai dettagli implementativi del database e offre un'interfaccia orientata agli oggetti per lavorare con i dati.

class OrderRepository
{
	public function find(int $id): ?Order
	{
	}

	public function findByCustomer(int $customerId): array
	{
	}
}

Entità: oggetti che rappresentano i principali concetti di business dell'applicazione, che hanno una propria identità e cambiano nel tempo. Di norma sono classi mappate sulle tabelle del database tramite un ORM (come Nette Database Explorer o Doctrine). Le entità possono contenere regole di business relative ai propri dati e logica di validazione.

// entità mappata sulla tabella di database '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,
		]);
	}
}

Value object: oggetti immutabili che rappresentano valori privi di identità propria, per esempio un importo monetario o un indirizzo e-mail. Due istanze di un value object con gli stessi valori sono considerate identiche.

Codice infrastrutturale

La cartella Core/ (o, in alternativa, Infrastructure/) ospita le fondamenta tecniche dell'applicazione. Il codice infrastrutturale comprende di norma:

app/Core/
├── Router/               ← routing e gestione degli URL
│   └── RouterFactory.php
├── Security/             ← autenticazione e autorizzazione
│   ├── Authenticator.php
│   └── Authorizator.php
├── Logging/              ← logging e monitoraggio
│   ├── SentryLogger.php
│   └── FileLogger.php
├── Cache/                ← strato di caching
│   └── FullPageCache.php
└── Integration/          ← integrazione con servizi esterni
	├── Slack/
	└── Stripe/

Per i progetti più piccoli basta naturalmente una struttura piatta:

Core/
├── RouterFactory.php
├── Authenticator.php
└── QueueMailer.php

È il codice che:

  • si occupa dell'infrastruttura tecnica (routing, logging, caching)
  • integra servizi esterni (Sentry, Elasticsearch, Redis)
  • offre servizi di base all'intera applicazione (posta, database)
  • è per lo più indipendente da un dominio specifico: la cache o il logger funzionano allo stesso modo per un e-shop o per un blog.

Vi state chiedendo se una certa classe appartenga a questa cartella o al model? La differenza fondamentale è che il codice in Core/:

  • non sa nulla del dominio (prodotti, ordini, articoli)
  • di norma si può trasferire in un altro progetto
  • risolve il “come funziona” (come inviare un'e-mail), non il “cosa fa” (quale e-mail inviare)

Un esempio per capire meglio:

  • App\Core\MailerFactory – crea istanze della classe per inviare e-mail, gestisce le impostazioni SMTP
  • App\Model\OrderMailer – usa MailerFactory per inviare le e-mail relative agli ordini, ne conosce i template e sa quando vanno inviate

Script da riga di comando

Le applicazioni hanno spesso bisogno di svolgere attività fuori dalle normali richieste HTTP: che si tratti di elaborazione dati in background, di manutenzione o di attività periodiche. Per l'esecuzione si usano semplici script nella directory bin/, mentre la logica implementativa vera e propria si colloca in app/Tasks/ (o in app/Commands/).

Esempio:

app/Tasks/
├── Maintenance/               ← script di manutenzione
│   ├── CleanupCommand.php     ← eliminazione dei dati vecchi
│   └── DbOptimizeCommand.php  ← ottimizzazione del database
├── Integration/               ← integrazione con sistemi esterni
│   ├── ImportProducts.php     ← importazione dal sistema del fornitore
│   └── SyncOrders.php         ← sincronizzazione degli ordini
└── Scheduled/                 ← attività periodiche
	├── NewsletterCommand.php  ← invio delle newsletter
	└── ReminderCommand.php    ← notifiche ai clienti

Cosa appartiene al model e cosa agli script da riga di comando? Per esempio la logica per inviare una singola e-mail fa parte del model, mentre l'invio massivo di migliaia di e-mail appartiene a Tasks/.

I task si eseguono di solito dalla riga di comando o via cron: lo script in bin/ crea il container DI con il metodo bootConsoleApplication() e ne estrae il servizio necessario. Si possono eseguire anche tramite una richiesta HTTP, ma bisogna pensare alla sicurezza. Il presenter che esegue il task va protetto, per esempio consentendolo solo agli utenti connessi oppure con un token robusto e l'accesso da indirizzi IP autorizzati. Per i task di lunga durata è necessario aumentare il limite di tempo dello script e usare session_write_close() per non bloccare la sessione.

Altre directory possibili

Oltre alle directory di base già menzionate, potete aggiungere altre cartelle specializzate secondo le esigenze del progetto. Vediamo le più comuni e il loro uso:

app/
├── Api/              ← logica dell'API, indipendente dallo strato di presentazione
├── Database/         ← script di migrazione e seeder per i dati di test
├── Components/       ← componenti visivi condivisi in tutta l'applicazione
├── Event/            ← utile se usate un'architettura a eventi
├── Mail/             ← template delle e-mail e logica collegata
└── Utils/            ← classi di supporto

Per i componenti visivi condivisi, usati nei presenter di tutta l'applicazione, potete usare la cartella app/Components o app/Controls:

app/Components/
├── Form/                 ← componenti di form condivisi
│   ├── SignInForm.php
│   └── UserForm.php
├── Grid/                 ← componenti per gli elenchi di dati
│   └── DataGrid.php
└── Navigation/           ← elementi di navigazione
	├── Breadcrumbs.php
	└── Menu.php

È qui che appartengono i componenti con logica più complessa. Se volete condividere i componenti tra più progetti, conviene estrarli in un pacchetto Composer separato.

Nella directory app/Mail potete collocare la gestione della comunicazione via e-mail:

app/Mail/
├── templates/            ← template delle e-mail
│   ├── order-confirmation.latte
│   └── welcome.latte
└── OrderMailer.php

Mapping dei presenter

Il mapping definisce le regole per ricavare il nome della classe dal nome del presenter. Le indichiamo nella configurazione, sotto la chiave application › mapping.

In questa pagina abbiamo mostrato che collochiamo i presenter nella cartella app/Presentation (o app/UI). Da Nette Application 3.3 questa è la convenzione predefinita, che non serve configurare. Se usate una struttura diversa o volete indicare il mapping esplicitamente, l'impostazione predefinita corrisponde a questa riga:

application:
	mapping: App\Presentation\*\**Presenter

Come funziona il mapping? Per capire meglio, immaginiamo prima un'applicazione senza moduli. Vogliamo che le classi dei presenter ricadano nel namespace App\Presentation, così che il presenter Home sia mappato sulla classe App\Presentation\HomePresenter. Lo si ottiene con questa configurazione:

application:
	mapping: App\Presentation\*Presenter

Il mapping funziona sostituendo l'asterisco nella maschera App\Presentation\*Presenter con il nome del presenter Home, ottenendo il nome finale della classe App\Presentation\HomePresenter. Semplice!

Come vedete negli esempi di questo e di altri capitoli, però, collochiamo le classi dei presenter in sottodirectory omonime: per esempio il presenter Home è mappato sulla classe App\Presentation\Home\HomePresenter. Lo otteniamo usando il doppio asterisco ** (richiede Nette Application 3.2.3):

application:
	mapping: App\Presentation\**Presenter

Passiamo ora al mapping dei presenter nei moduli. Possiamo definire un mapping specifico per ogni modulo:

application:
	mapping:
		Front: App\Presentation\Front\**Presenter
		Admin: App\Presentation\Admin\**Presenter
		Api: App\Api\*Presenter

Secondo questa configurazione il presenter Front:Home è mappato sulla classe App\Presentation\Front\Home\HomePresenter, mentre il presenter Api:OAuth è mappato sulla classe App\Api\OAuthPresenter.

Poiché i moduli Front e Admin hanno uno schema di mapping simile, e di moduli così ce ne saranno probabilmente altri, è possibile creare una regola generale che li sostituisca. Nella maschera della classe si aggiunge un nuovo asterisco per il modulo:

application:
	mapping:
		*: App\Presentation\*\**Presenter
		Api: App\Api\*Presenter

Funziona anche per strutture di directory annidate più in profondità, come il presenter Admin:User:Edit, dove il segmento con l'asterisco si ripete per ogni livello di modulo, dando come risultato la classe App\Presentation\Admin\User\Edit\EditPresenter.

Una notazione alternativa è usare, al posto di una stringa, un array composto da tre segmenti. Per gli esempi mostrati sopra questa notazione equivale alla precedente:

application:
	mapping:
		*: [App\Presentation, *, **Presenter]
		Api: [App\Api, '', *Presenter]
versione: 4.x