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
.htaccesscon 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 SMTPApp\Model\OrderMailer– usaMailerFactoryper 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]