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 vostro codice. Imparerete:

  • come strutturare logicamente l'applicazione in directory
  • come progettare la struttura per scalare bene con la crescita del progetto
  • quali sono le possibili alternative e i loro vantaggi o svantaggi

È importante ricordare che Nette Framework non insiste su alcuna struttura specifica. È stato progettato per essere facilmente adattabile a qualsiasi esigenza e preferenza.

Struttura di base del progetto

Sebbene Nette Framework non imponga una struttura di directory fissa, esiste una disposizione predefinita e collaudata sotto forma di Web Project:

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

È possibile modificare liberamente questa struttura in base alle proprie esigenze, rinominando o spostando le cartelle. È sufficiente modificare i percorsi relativi alle cartelle in Bootstrap.php ed eventualmente in composer.json. Non serve nient'altro, nessuna riconfigurazione complessa, nessuna modifica costante. Nette ha un rilevamento automatico intelligente e riconosce automaticamente la posizione dell'applicazione, compresa la sua base URL.

Principi di organizzazione del codice

Quando si esplora per la prima volta un nuovo progetto, si dovrebbe essere in grado di orientarsi rapidamente. Immaginate di fare clic sulla cartella app/Model/ e di vedere questa struttura:

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

Da qui si apprende solo che il progetto utilizza alcuni servizi, repository ed entità. Non si apprende nulla sullo scopo effettivo dell'applicazione.

Vediamo un approccio diverso: organizzazione per domini:

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

Questo è diverso: a prima vista è chiaro che si tratta di un sito di e-commerce. I nomi stessi delle directory rivelano ciò che l'applicazione può fare: lavora con pagamenti, ordini e prodotti.

Il primo approccio (organizzazione per tipo di classe) comporta diversi problemi nella pratica: il codice che è logicamente correlato è sparso in diverse cartelle e bisogna saltare da una all'altra. Pertanto, organizzeremo per domini.

Spazi dei nomi

È convenzionale che la struttura delle directory corrisponda agli spazi dei nomi nell'applicazione. Ciò significa che la posizione fisica dei file corrisponde al loro spazio dei nomi. Per esempio, una classe situata in app/Model/Product/ProductRepository.php dovrebbe avere lo spazio dei nomi App\Model\Product. Questo principio aiuta a orientare il codice e semplifica il caricamento automatico.

Singolare e plurale nei nomi

Si noti che usiamo il singolare per le directory delle applicazioni principali: app, config, log, temp, www. Lo stesso vale all'interno dell'applicazione: Model, Core, Presentation. Questo perché ognuno di essi rappresenta un concetto unificato.

Allo stesso modo, app/Model/Product rappresenta tutto ciò che riguarda i prodotti. Non lo chiamiamo Products perché non è una cartella piena di prodotti (che conterrebbe file come iphone.php, samsung.php). È uno spazio dei nomi che contiene classi per lavorare con i prodotti – ProductRepository.php, ProductService.php.

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

Per coerenza, si consiglia di usare:

  • Singolare per gli spazi dei nomi che rappresentano un'unità funzionale (anche se si lavora con più entità).
  • Plurale per le collezioni di unità indipendenti
  • In caso di incertezza o se non si vuole pensarci, scegliere singolare

Elenco pubblico www/

Questa directory è l'unica accessibile dal web (la cosiddetta document-root). Spesso si può trovare il nome public/ invece di www/: è solo una questione di convenzione e non influisce sulla funzionalità. La directory contiene:

  • Punto di ingresso dell'applicazione index.php
  • File .htaccess con regole di mod_rewrite (per Apache)
  • File statici (CSS, JavaScript, immagini)
  • File caricati

Per una corretta sicurezza dell'applicazione, è fondamentale avere una document-root configurata correttamente.

Non collocare mai la cartella node_modules/ in questa directory: contiene migliaia di file che possono essere eseguibili e non dovrebbero essere accessibili al pubblico.

Directory delle applicazioni app/

Questa è la directory principale con il codice dell'applicazione. Struttura di base:

app/
├── Core/               ← L'infrastruttura è importante
├── Model/              ← logica aziendale
├── Presentation/       ← presentatori e modelli
├── Tasks/              ← script di comando
└── Bootstrap.php       ← classe bootstrap dell'applicazione

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

Vediamo ora in dettaglio le singole sottodirectory.

Presentatori e modelli

La parte di presentazione dell'applicazione si trova nella directory app/Presentation. Un'alternativa è la breve app/UI. Qui si trovano tutti i presentatori, i loro modelli e tutte le classi di aiuto.

Organizziamo questo livello per domini. In un progetto complesso, che combina e-commerce, blog e API, la struttura sarebbe la seguente:

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

Al contrario, per un semplice blog utilizzeremmo questa struttura:

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

Cartelle come Home/ o Dashboard/ contengono presentatori e modelli. Cartelle come Front/, Admin/ o Api/ sono chiamate moduli. Tecnicamente, si tratta di cartelle regolari che servono per l'organizzazione logica dell'applicazione.

Ogni cartella con un presentatore contiene un presentatore con nome simile e i relativi modelli. Ad esempio, la cartella Dashboard/ contiene:

Dashboard/
├── DashboardPresenter.php     ← presentatore
└── default.latte              ← modello

Questa struttura di directory si riflette negli spazi dei nomi delle classi. Ad esempio, DashboardPresenter si trova nello spazio dei nomi App\Presentation\Admin\Dashboard (vedere la mappatura dei presentatori):

namespace App\Presentation\Admin\Dashboard;

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

Ci riferiamo al presentatore Dashboard all'interno del modulo Admin nell'applicazione usando la notazione dei due punti come Admin:Dashboard. Alla sua azione default ci si riferisce quindi come Admin:Dashboard:default. Per i moduli annidati, usiamo più punti, ad esempio Shop:Order:Detail:default.

Sviluppo di una struttura flessibile

Uno dei grandi vantaggi di questa struttura è l'eleganza con cui si adatta alle crescenti esigenze del progetto. A titolo di esempio, prendiamo la parte che genera i feed XML. Inizialmente, abbiamo un semplice modulo:

Export/
├── ExportPresenter.php   ← un unico presentatore per tutte le esportazioni
├── sitemap.latte         ← modello per la mappa del sito
└── feed.latte            ← modello per il feed RSS

Nel corso del tempo, vengono aggiunti altri tipi di feed e abbiamo bisogno di più logica per loro… 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 agevole: basta creare nuove sottocartelle, suddividervi il codice e aggiornare i collegamenti (ad esempio, da Export:feed a Export:Feed:amazon). Grazie a ciò, possiamo espandere gradualmente la struttura secondo le necessità, il livello di annidamento non è limitato in alcun modo.

Ad esempio, se nell'amministrazione sono presenti molti presentatori relativi alla gestione degli ordini, come OrderDetail, OrderEdit, OrderDispatch ecc. si può creare un modulo (cartella) Order per una migliore organizzazione, che conterrà (cartelle per) i presentatori Detail, Edit, Dispatch e altri.

Posizione del modello

Negli esempi precedenti, abbiamo visto che i modelli si trovano direttamente nella cartella del presentatore:

Dashboard/
├── DashboardPresenter.php     ← presentatore
├── DashboardTemplate.php      ← classe modello opzionale
└── default.latte              ← modello

Questa posizione si rivela la più comoda nella pratica: si hanno tutti i file correlati a portata di mano.

In alternativa, è possibile collocare i modelli in una sottocartella di templates/. Nette supporta entrambe le varianti. È anche possibile collocare i modelli completamente al di fuori della cartella Presentation/. Per maggiori informazioni sulle opzioni di collocazione dei modelli, consultare il capitolo Ricerca dei modelli.

Classi e componenti di aiuto

I presentatori e i modelli sono spesso accompagnati da altri file di aiuto. Li collochiamo logicamente in base al loro scopo:

1. Direttamente con il presentatore nel caso di componenti specifici per un determinato presentatore:

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

2. Per il modulo – si consiglia di utilizzare la cartella Accessory, che si trova ordinatamente all'inizio dell'alfabeto:

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/

Oppure si possono inserire classi di aiuto come LatteExtension.php o TemplateFilters.php nella cartella dell'infrastruttura app/Core/Latte/. E i componenti in app/Components. La scelta dipende dalle convenzioni del team.

Modello – Cuore dell'applicazione

Il modello contiene tutta la logica di business dell'applicazione. Per la sua organizzazione, vale la stessa regola: si struttura per domini:

app/Model/
├── Payment/                   ← tutto sui pagamenti
│   ├── PaymentFacade.php      ← punto di ingresso principale
│   ├── PaymentRepository.php
│   ├── Payment.php            ← entità
├── Order/                     ← tutto sugli ordini
│   ├── OrderFacade.php
│   ├── OrderRepository.php
│   ├── Order.php
└── Shipping/                  ← tutto sulla spedizione

Nel modello si incontrano tipicamente questi tipi di classi:

Facades: rappresentano il punto di ingresso principale in un dominio specifico dell'applicazione. Agiscono come un orchestratore che coordina la cooperazione tra diversi servizi per implementare casi d'uso completi (come “creare un ordine” o “elaborare un pagamento”). Sotto il loro livello di orchestrazione, la facciata nasconde i dettagli dell'implementazione al resto dell'applicazione, fornendo così un'interfaccia pulita per lavorare con il dominio in questione.

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

Servizi: si concentrano su operazioni commerciali specifiche all'interno di un dominio. A differenza delle facciate, che orchestrano interi casi d'uso, un servizio implementa una logica aziendale specifica (come il calcolo dei prezzi o l'elaborazione dei pagamenti). I servizi sono tipicamente stateless e possono essere utilizzati sia dalle facade come elementi costitutivi per 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 tutte le comunicazioni con l'archivio dati, in genere un database. Il loro compito è quello di caricare e salvare le entità e di implementare metodi per la loro ricerca. Un repository protegge il resto dell'applicazione dai dettagli dell'implementazione del database e fornisce 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 loro identità e cambiano nel tempo. In genere si tratta di classi mappate sulle tabelle del database tramite ORM (come Nette Database Explorer o Doctrine). Le entità possono contenere regole di business relative ai loro dati e alla logica di validazione.

// Entità mappata alla tabella del database ordini
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,
		]);
	}
}

Oggetti valore: oggetti immutabili che rappresentano valori senza una propria identità, ad esempio una somma di denaro o un indirizzo e-mail. Due istanze di un oggetto valore con gli stessi valori sono considerate identiche.

Codice dell'infrastruttura

La cartella Core/ (o anche Infrastructure/) ospita le fondamenta tecniche dell'applicazione. Il codice dell'infrastruttura include tipicamente:

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

Per i progetti più piccoli, una struttura piatta è naturalmente sufficiente:

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

Questo è codice che:

  • Gestisce l'infrastruttura tecnica (routing, logging, caching)
  • integra servizi esterni (Sentry, Elasticsearch, Redis)
  • Fornisce servizi di base per l'intera applicazione (posta, database)
  • È per lo più indipendente dal dominio specifico: la cache o il logger funzionano allo stesso modo per l'e-commerce o il blog.

Ci si chiede se una certa classe debba stare qui o nel modello? La differenza fondamentale è che il codice in Core/:

  • Non sa nulla del dominio (prodotti, ordini, articoli).
  • Di solito può essere trasferito a un altro progetto
  • Risolve “come funziona” (come inviare la posta), non “cosa fa” (quale posta inviare)

Esempio per una migliore comprensione:

  • App\Core\MailerFactory – crea istanze di classe per l'invio di email, gestisce le impostazioni SMTP
  • App\Model\OrderMailer – utilizza MailerFactory per inviare le e-mail sugli ordini, conosce i loro modelli e quando devono essere inviati

Script di comando

Le applicazioni hanno spesso bisogno di eseguire compiti al di fuori delle normali richieste HTTP, che si tratti di elaborazione di dati in background, manutenzione o compiti periodici. Per l'esecuzione vengono utilizzati semplici script nella cartella bin/, mentre la logica di implementazione vera e propria è collocata in app/Tasks/ (o app/Commands/).

Esempio:

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

Cosa appartiene al modello e cosa agli script di comando? Ad esempio, la logica per l'invio di un'e-mail fa parte del modello, l'invio massivo di migliaia di e-mail appartiene a Tasks/.

I task vengono solitamente eseguiti dalla riga di comando o tramite cron. Possono anche essere eseguiti tramite richiesta HTTP, ma occorre tenere conto della sicurezza. Il presenter che esegue il task deve essere protetto, ad esempio solo per gli utenti loggati o con un token forte e l'accesso da indirizzi IP consentiti. Per i task lunghi, è necessario aumentare il limite di tempo dello script e utilizzare session_write_close() per evitare di bloccare la sessione.

Altre possibili directory

Oltre alle directory di base menzionate, è possibile aggiungere altre cartelle specializzate in base alle esigenze del progetto. Vediamo le più comuni e il loro utilizzo:

app/
├── Api/              ← Logica API indipendente dal livello di presentazione
├── Database/         ← script di migrazione e seeders per i dati di test
├── Components/       ← componenti visivi condivisi nell'applicazione
├── Event/            ← utile se si utilizza un'architettura guidata dagli eventi
├── Mail/             ← modelli di e-mail e relativa logica
└── Utils/            ← classi di aiuto

Per i componenti visivi condivisi usati nelle presentazioni di tutta l'applicazione, si può usare la cartella app/Components o app/Controls:

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

Qui si trovano i componenti con una logica più complessa. Se si desidera condividere i componenti tra più progetti, è bene separarli in un pacchetto autonomo del compositore.

Nella cartella app/Mail si può collocare la gestione delle comunicazioni via e-mail:

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

Mappatura dei presentatori

La mappatura definisce le regole per derivare i nomi delle classi dai nomi dei presentatori. Vengono specificate nella configurazione sotto la chiave application › mapping.

In questa pagina, abbiamo mostrato che collochiamo i presentatori nella cartella app/Presentation (o app/UI). Dobbiamo comunicare a Nette questa convenzione nel file di configurazione. Una riga è sufficiente:

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

Come funziona la mappatura? Per capire meglio, immaginiamo prima un'applicazione senza moduli. Vogliamo che le classi del presentatore rientrino nello spazio dei nomi App\Presentation, in modo che il presentatore Home sia mappato sulla classe App\Presentation\HomePresenter. Questo si ottiene con questa configurazione:

application:
	mapping: App\Presentation\*Presenter

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

Tuttavia, come si vede negli esempi di questo e di altri capitoli, le classi del presentatore vengono collocate in sottodirectory eponime, ad esempio il presentatore Home viene mappato nella classe App\Presentation\Home\HomePresenter. Questo si ottiene raddoppiando i due punti (richiede Nette Application 3.2):

application:
	mapping: App\Presentation\**Presenter

Ora passiamo alla mappatura dei presentatori nei moduli. Possiamo definire una mappatura specifica per ogni modulo:

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

In base a questa configurazione, il presentatore Front:Home si mappa alla classe App\Presentation\Front\Home\HomePresenter, mentre il presentatore Api:OAuth si mappa alla classe App\Api\OAuthPresenter.

Poiché i moduli Front e Admin hanno un metodo di mappatura simile e probabilmente ci saranno altri moduli di questo tipo, è possibile creare una regola generale che li sostituisca. Un nuovo asterisco per il modulo sarà aggiunto alla maschera della classe:

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 e risulta nella classe App\Presentation\Admin\User\Edit\EditPresenter.

Una notazione alternativa consiste nell'utilizzare un array composto da tre segmenti invece di una stringa. Questa notazione è equivalente alla precedente:

application:
	mapping:
		*: [App\Presentation, *, **Presenter]
		Api: [App\Api, '', *Presenter]
versione: 4.0