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 SMTPApp\Model\OrderMailer
– utilizzaMailerFactory
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]