Struttura della Directory dell'Applicazione
Come progettare una struttura di directory chiara e scalabile per i progetti in Nette Framework? Mostreremo le best practice che ti aiuteranno a organizzare il codice. Imparerai:
- come dividere logicamente l'applicazione in directory
- come progettare la struttura in modo che scali bene con la crescita del progetto
- quali sono le alternative possibili e i loro vantaggi o svantaggi
È importante menzionare che Nette Framework stesso non impone alcuna struttura specifica. È progettato per essere facilmente adattabile a qualsiasi esigenza e preferenza.
Struttura di base del progetto
Sebbene Nette Framework non detti alcuna struttura di directory fissa, esiste una disposizione predefinita comprovata sotto forma di Web Project:
web-project/ ├── app/ ← directory con l'applicazione ├── assets/ ← file SCSS, JS, immagini..., alternativamente 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)
Puoi modificare liberamente questa struttura in base alle tue esigenze – rinominare o spostare le cartelle.
Successivamente, basta solo aggiornare i percorsi relativi alle directory nel file Bootstrap.php
e eventualmente
composer.json
. Non è necessario nient'altro, nessuna riconfigurazione complessa, nessuna modifica delle costanti.
Nette dispone di un intelligente autodetect e riconosce automaticamente la posizione dell'applicazione, inclusa la sua
base URL.
Principi di organizzazione del codice
Quando esplori per la prima volta un nuovo progetto, dovresti orientarti rapidamente. Immagina di espandere la directory
app/Model/
e vedere questa struttura:
app/Model/ ├── Services/ ├── Repositories/ └── Entities/
Da essa deduci solo che il progetto utilizza alcuni servizi, repository ed entità. Non impari assolutamente nulla sullo scopo effettivo dell'applicazione.
Vediamo un approccio diverso – organizzazione per domini:
app/Model/ ├── Cart/ ├── Payment/ ├── Order/ └── Product/
Qui è diverso – a prima vista è 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 (organizzazione per tipo di classi) porta in pratica una serie di problemi: il codice che è logicamente correlato è frammentato in diverse cartelle e devi saltare tra di esse. Pertanto, organizzeremo per domini.
Namespace
È consuetudine che la struttura delle directory corrisponda ai namespace nell'applicazione. Ciò significa che la posizione
fisica dei file corrisponde al loro namespace. Ad esempio, una classe situata in
app/Model/Product/ProductRepository.php
dovrebbe avere il namespace App\Model\Product
. Questo principio
aiuta nell'orientamento nel codice e semplifica l'autoloading.
Singolare vs Plurale nei nomi
Nota che per le directory principali dell'applicazione usiamo il singolare: app
, config
,
log
, temp
, www
. Allo stesso modo anche all'interno dell'applicazione: Model
,
Core
, Presentation
. Questo perché ognuna di esse rappresenta un concetto unitario.
Allo stesso modo, ad esempio, app/Model/Product
rappresenta tutto ciò che riguarda i prodotti. Non lo chiameremo
Products
, perché non è una cartella piena di prodotti (ci sarebbero file nokia.php
,
samsung.php
). È un namespace contenente 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à separata.
Per coerenza, consigliamo di utilizzare:
- Singolare per namespace che rappresentano un'unità funzionale (anche se lavorano con più entità)
- Plurale per collezioni di unità separate
- In caso di incertezza o se non vuoi pensarci, scegli il singolare
Directory pubblica www/
Questa directory è l'unica accessibile dal web (la cosiddetta document-root). Spesso si può incontrare anche il nome
public/
invece di www/
– è solo una questione di convenzione e non influisce sulla funzionalità del
framework. La directory contiene:
- Punto di ingresso dell'applicazione
index.php
- File
.htaccess
con regole per mod_rewrite (per Apache) - File statici (CSS, JavaScript, immagini)
- File caricati
Per una corretta sicurezza dell'applicazione, è fondamentale avere la document-root configurata correttamente.
Non posizionare mai la cartella node_modules/
in questa directory – contiene migliaia di file che
possono essere eseguibili e non dovrebbero essere accessibili pubblicamente.
Directory dell'applicazione app/
Questa è la directory principale con il codice dell'applicazione. Struttura di base:
app/ ├── Core/ ← questioni infrastrutturali ├── Model/ ← logica di business ├── Presentation/ ← presenter e template ├── Tasks/ ← script 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ù nel dettaglio le singole sottodirectory.
Presenter e template
La parte di presentazione dell'applicazione si trova nella directory app/Presentation
. Un'alternativa è la breve
app/UI
. È il posto per tutti i presenter, i loro template e eventuali classi di supporto.
Organizziamo questo layer per domini. In un progetto complesso che combina e-shop, blog e API, la struttura sarebbe simile a questa:
app/Presentation/ ├── Shop/ ← frontend e-shop │ ├── Product/ │ ├── Cart/ │ └── Order/ ├── Blog/ ← blog │ ├── Home/ │ └── Post/ ├── Admin/ ← amministrazione │ ├── Dashboard/ │ └── Products/ └── Api/ ← endpoint API └── V1/
Al contrario, per un semplice blog, useremmo la seguente suddivisione:
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/
le chiamiamo moduli. Tecnicamente, sono directory normali che servono a dividere
logicamente l'applicazione.
Ogni cartella con un presenter contiene un presenter con lo stesso nome e i suoi template. Ad esempio, la cartella
Dashboard/
contiene:
Dashboard/ ├── DashboardPresenter.php ← presenter └── default.latte ← template
Questa struttura di directory si riflette nei namespace delle classi. Ad esempio, DashboardPresenter
si trova nel
namespace App\Presentation\Admin\Dashboard
(vedi mapování presenterů):
namespace App\Presentation\Admin\Dashboard;
class DashboardPresenter extends Nette\Application\UI\Presenter
{
// ...
}
Al presenter Dashboard
all'interno del modulo Admin
facciamo riferimento nell'applicazione usando la
notazione con i due punti come Admin:Dashboard
. Alla sua azione default
poi come
Admin:Dashboard:default
. In caso di moduli nidificati, usiamo più due punti, ad esempio
Shop:Order:Detail:default
.
Sviluppo flessibile della struttura
Uno dei grandi vantaggi di questa struttura è come si adatta elegantemente alle crescenti esigenze del progetto. Prendiamo come esempio la parte che genera feed XML. All'inizio abbiamo una forma semplice:
Export/ ├── ExportPresenter.php ← un presenter per tutte le esportazioni ├── sitemap.latte ← template per la sitemap └── feed.latte ← template per il feed RSS
Con il tempo, si aggiungono altri tipi di feed e abbiamo bisogno di più logica per essi… Nessun problema! La cartella
Export/
diventa semplicemente un modulo:
Export/ ├── Sitemap/ │ ├── SitemapPresenter.php │ └── sitemap.latte └── Feed/ ├── FeedPresenter.php ├── zbozi.latte ← feed per Zboží.cz └── heureka.latte ← feed per Heureka.cz
Questa trasformazione è assolutamente fluida – basta creare nuove sottocartelle, dividerci il codice e aggiornare i link
(ad esempio da Export:feed
a Export:Feed:zbozi
). Grazie a ciò, possiamo espandere gradualmente la
struttura secondo necessità, il livello di nidificazione non è limitato in alcun modo.
Se, ad esempio, nell'amministrazione hai molti presenter relativi alla gestione degli ordini, come sono
OrderDetail
, OrderEdit
, OrderDispatch
ecc., puoi creare un modulo (cartella)
Order
in questo punto per una migliore organizzazione, che conterrà (le cartelle per) i presenter
Detail
, Edit
, Dispatch
e altri.
Posizionamento dei template
Negli esempi precedenti abbiamo visto che i template si trovano direttamente nella cartella con il presenter:
Dashboard/ ├── DashboardPresenter.php ← presenter ├── DashboardTemplate.php ← classe opzionale per il template └── default.latte ← template
Questa posizione si rivela in pratica la più comoda – hai tutti i file correlati subito a portata di mano.
In alternativa, puoi posizionare i template in una sottocartella templates/
. Nette supporta entrambe le varianti.
Puoi persino posizionare i template completamente al di fuori della cartella Presentation/
. Tutto sulle possibilità
di posizionamento dei template si trova nel capitolo Ricerca dei template.
Classi di supporto e componenti
Ai presenter e ai template spesso appartengono anche altri file di supporto. Li posizioniamo logicamente in base al loro ambito di applicazione:
1. Direttamente presso il presenter nel caso di componenti specifici per 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 utilizzare la cartella Accessory
, che si posiziona 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 puoi posizionare 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
abitudini del team.
Model – il cuore dell'applicazione
Il model contiene tutta la logica di business dell'applicazione. Per la sua organizzazione vale di nuovo la regola – strutturiamo per domini:
app/Model/ ├── Payment/ ← tutto ciò che riguarda i pagamenti │ ├── PaymentFacade.php ← punto di 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 la spedizione
Nel model si incontrano tipicamente questi tipi di classi:
Facade: rappresentano il punto di ingresso principale a un dominio specifico nell'applicazione. Agiscono come orchestratori che coordinano la collaborazione tra diversi servizi allo scopo di implementare use-case completi (come “crea ordine” o “elabora pagamento”). Sotto il suo layer di orchestrazione, la facade nasconde i dettagli implementativi al resto dell'applicazione, fornendo così un'interfaccia pulita per lavorare con il dominio dato.
class OrderFacade
{
public function createOrder(Cart $cart): Order
{
// validazione
// creazione dell'ordine
// invio dell'e-mail
// scrittura nelle statistiche
}
}
Servizi: si concentrano su un'operazione di business specifica all'interno del dominio. A differenza della facade, che orchestra interi use-case, un servizio implementa una logica di business specifica (come calcoli di prezzi o elaborazione di pagamenti). I servizi sono tipicamente senza stato e possono essere utilizzati sia dalle facade come blocchi di costruzione 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: assicurano tutta la comunicazione con l'archivio dati, tipicamente un database. Il suo compito è caricare e salvare entità e implementare metodi per la loro ricerca. Il repository isola il resto dell'applicazione dai dettagli implementativi 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 nell'applicazione, che hanno una loro identità e cambiano nel tempo. Tipicamente si tratta di classi mappate su tabelle di database tramite ORM (come Nette Database Explorer o Doctrine). Le entità possono contenere regole di business relative ai loro 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 senza una propria identità – ad 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 anche Infrastructure/
) è la casa della base tecnica dell'applicazione. Il
codice infrastrutturale include tipicamente:
app/Core/ ├── Router/ ← routing e gestione URL │ └── RouterFactory.php ├── Security/ ← autenticazione e autorizzazione │ ├── Authenticator.php │ └── Authorizator.php ├── Logging/ ← logging e monitoraggio │ ├── SentryLogger.php │ └── FileLogger.php ├── Cache/ ← layer di caching │ └── FullPageCache.php └── Integration/ ← integrazione con servizi est. ├── Slack/ └── Stripe/
Per progetti più piccoli, ovviamente, basta una suddivisione piatta:
Core/ ├── RouterFactory.php ├── Authenticator.php └── QueueMailer.php
Si tratta di codice che:
- Risolve l'infrastruttura tecnica (routing, logging, caching)
- Integra servizi esterni (Sentry, Elasticsearch, Redis)
- Fornisce servizi di base per l'intera applicazione (mail, database)
- È per lo più indipendente dal dominio specifico – la cache o il logger funzionano allo stesso modo per un eshop o un blog.
Hai dubbi se una certa classe appartiene qui o al model? La differenza chiave è che il codice in Core/
:
- Non sa nulla del dominio (prodotti, ordini, articoli)
- È per lo più possibile trasferirlo a un altro progetto
- Risolve “come funziona” (come inviare una mail), non “cosa fa” (quale mail inviare)
Esempio per una migliore comprensione:
App\Core\MailerFactory
– crea istanze della classe per l'invio di e-mail, gestisce le impostazioni SMTPApp\Model\OrderMailer
– utilizzaMailerFactory
per inviare e-mail sugli ordini, conosce i loro template e sa quando devono essere inviati
Script di comando
Le applicazioni spesso necessitano di eseguire attività al di fuori delle normali richieste HTTP – che si tratti di
elaborazione dati in background, manutenzione o attività periodiche. Per l'esecuzione servono semplici script nella directory
bin/
, la logica implementativa la posizioniamo poi in app/Tasks/
(eventualmente
app/Commands/
).
Esempio:
app/Tasks/ ├── Maintenance/ ← script di manutenzione │ ├── CleanupCommand.php ← cancellazione di 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à regolari ├── NewsletterCommand.php ← invio di newsletter └── ReminderCommand.php ← notifiche ai clienti
Cosa appartiene al model e cosa agli script di comando? Ad esempio, la logica per l'invio di una singola e-mail fa parte del
model, l'invio massivo di migliaia di e-mail appartiene già a Tasks/
.
Le attività vengono solitamente eseguite dalla riga di
comando o tramite cron. Possono essere eseguite anche tramite richiesta HTTP, ma è necessario pensare alla sicurezza. Il
presenter che avvia l'attività deve essere protetto, ad esempio solo per utenti loggati o con un token forte e accesso da
indirizzi IP consentiti. Per attività lunghe è necessario aumentare il limite di tempo dello script e utilizzare
session_write_close()
, per non bloccare la sessione.
Altre possibili directory
Oltre alle directory di base menzionate, puoi aggiungere altre cartelle specializzate in base alle esigenze del progetto. Vediamo le più comuni e il loro utilizzo:
app/ ├── Api/ ← logica per API indipendente dal layer di presentazione ├── Database/ ← script di migrazione e seeder per dati di test ├── Components/ ← componenti visivi condivisi in tutta l'applicazione ├── Event/ ← utile se usi architettura event-driven ├── Mail/ ← template e-mail e logica correlata └── Utils/ ← classi di utilità
Per i componenti visivi condivisi utilizzati nei presenter in tutta l'applicazione, è possibile utilizzare la cartella
app/Components
o app/Controls
:
app/Components/ ├── Form/ ← componenti form condivisi │ ├── SignInForm.php │ └── UserForm.php ├── Grid/ ← componenti per elenchi di dati │ └── DataGrid.php └── Navigation/ ← elementi di navigazione ├── Breadcrumbs.php └── Menu.php
Qui appartengono i componenti che hanno una logica più complessa. Se vuoi condividere componenti tra più progetti, è consigliabile estrarli in un pacchetto composer separato.
Nella directory app/Mail
puoi posizionare la gestione della comunicazione e-mail:
app/Mail/ ├── templates/ ← template e-mail │ ├── order-confirmation.latte │ └── welcome.latte └── OrderMailer.php
Mappatura dei presenter
La mappatura definisce le regole per derivare il nome della classe dal nome del presenter. Le specifichiamo nella configurazione sotto la chiave
application › mapping
.
In questa pagina abbiamo mostrato che posizioniamo i presenter nella cartella app/Presentation
(eventualmente
app/UI
). Dobbiamo comunicare questa convenzione a Nette nel file di configurazione. Basta una riga:
application:
mapping: App\Presentation\*\**Presenter
Come funziona la mappatura? Per una migliore comprensione, immaginiamo prima un'applicazione senza moduli. Vogliamo che le
classi dei presenter rientrino nel namespace App\Presentation
, in modo che il presenter Home
si mappi
sulla classe App\Presentation\HomePresenter
. Cosa che otteniamo con questa configurazione:
application:
mapping: App\Presentation\*Presenter
La mappatura funziona in modo che il nome del presenter Home
sostituisca l'asterisco nella maschera
App\Presentation\*Presenter
, ottenendo così il nome della classe risultante
App\Presentation\HomePresenter
. Semplice!
Come però vedi negli esempi in questo e altri capitoli, posizioniamo le classi dei presenter in sottodirectory omonime, ad
esempio il presenter Home
si mappa sulla classe App\Presentation\Home\HomePresenter
. Otteniamo ciò
raddoppiando i due punti (richiede Nette Application 3.2):
application:
mapping: App\Presentation\**Presenter
Ora passiamo alla mappatura dei presenter nei moduli. Per ogni modulo possiamo definire una mappatura specifica:
application:
mapping:
Front: App\Presentation\Front\**Presenter
Admin: App\Presentation\Admin\**Presenter
Api: App\Api\*Presenter
Secondo questa configurazione, il presenter Front:Home
si mappa sulla classe
App\Presentation\Front\Home\HomePresenter
, mentre il presenter Api:OAuth
sulla classe
App\Api\OAuthPresenter
.
Poiché i moduli Front
e Admin
hanno un modo simile di mappatura e probabilmente ci saranno più
moduli di questo tipo, è possibile creare una regola generale che li sostituisca. Alla maschera della classe si aggiunge così un
nuovo asterisco per il modulo:
application:
mapping:
*: App\Presentation\*\**Presenter
Api: App\Api\*Presenter
Funziona anche per strutture di directory più profondamente nidificate, come ad esempio il presenter
Admin:User:Edit
, il segmento con l'asterisco si ripete per ogni livello e il risultato è la classe
App\Presentation\Admin\User\Edit\EditPresenter
.
Una notazione alternativa è usare un array composto da tre segmenti invece di una stringa. Questa notazione è equivalente alla precedente:
application:
mapping:
*: [App\Presentation, *, **Presenter]
Api: [App\Api, '', *Presenter]