Structura de directoare a aplicației
Cum să proiectați o structură de directoare clară și scalabilă pentru proiectele din Nette Framework? Vă vom arăta practici dovedite care vă vor ajuta să vă organizați codul. Veți învăța:
- cum să structurați logic aplicația în directoare
- cum să proiectați structura astfel încât să scădească bine pe măsură ce proiectul crește
- care sunt alternativele posibile și avantajele sau dezavantajele acestora
Este important să menționăm că Nette Framework în sine nu insistă asupra unei structuri specifice. Este conceput pentru a fi ușor adaptabil la orice nevoi și preferințe.
Structura de bază a proiectului
Deși Nette Framework nu dictează o structură fixă a directoarelor, există un aranjament implicit dovedit sub forma Web Project:
web-project/ ├── app/ ← directorul aplicației ├── assets/ ← fișiere SCSS, JS, imagini..., alternativ resources/ ├── bin/ ← scripturi de linie de comandă ├── config/ ← configurare ├── log/ ← erori înregistrate ├── temp/ ← fișiere temporare, cache ├── tests/ ← teste ├── vendor/ ← biblioteci instalate de Composer └── www/ ← directorul public (document-root)
Puteți modifica liber această structură în funcție de nevoile dumneavoastră – redenumiți sau mutați foldere. Apoi
trebuie doar să ajustați căile relative către directoare în Bootstrap.php
și eventual
composer.json
. Nu este nevoie de nimic altceva, de nicio reconfigurare complexă, de nicio schimbare constantă.
Nette are autodetecție inteligentă și recunoaște automat locația aplicației, inclusiv baza sa URL.
Principii de organizare a codului
Atunci când explorați pentru prima dată un proiect nou, ar trebui să vă puteți orienta rapid. Imaginați-vă că faceți
clic pe directorul app/Model/
și vedeți această structură:
app/Model/ ├── Services/ ├── Repositories/ └── Entities/
Din aceasta, veți afla doar că proiectul utilizează anumite servicii, depozite și entități. Nu veți afla nimic despre scopul real al aplicației.
Să ne uităm la o abordare diferită – organizarea pe domenii:
app/Model/ ├── Cart/ ├── Payment/ ├── Order/ └── Product/
Acest lucru este diferit – la prima vedere este clar că acesta este un site de comerț electronic. Numele directoarelor în sine dezvăluie ce poate face aplicația – funcționează cu plăți, comenzi și produse.
Prima abordare (organizarea după tipul de clasă) aduce mai multe probleme în practică: codul care este legat logic este împrăștiat în diferite foldere și trebuie să săriți între ele. Prin urmare, vom organiza după domenii.
Spații de nume
Este convențional ca structura directoarelor să corespundă spațiilor de nume din aplicație. Aceasta înseamnă că
locația fizică a fișierelor corespunde spațiului de nume al acestora. De exemplu, o clasă localizată în
app/Model/Product/ProductRepository.php
ar trebui să aibă spațiul de nume App\Model\Product
. Acest
principiu ajută la orientarea codului și simplifică încărcarea automată.
Singular vs Plural în nume
Observați că folosim singularul pentru directoarele aplicațiilor principale: app
, config
,
log
, temp
, www
. Același lucru se aplică și în interiorul aplicației:
Model
, Core
, Presentation
. Acest lucru se datorează faptului că fiecare reprezintă un
concept unificat.
În mod similar, app/Model/Product
reprezintă totul despre produse. Nu îl numim Products
deoarece
nu este un folder plin cu produse (care ar conține fișiere precum iphone.php
, samsung.php
). Este un
spațiu de nume care conține clase pentru lucrul cu produsele – ProductRepository.php
,
ProductService.php
.
Dosarul app/Tasks
este la plural deoarece conține un set de scripturi executabile de sine stătătoare –
CleanupTask.php
, ImportTask.php
. Fiecare dintre ele este o unitate independentă.
Din motive de coerență, recomandăm utilizarea:
- Singular pentru spațiile de nume care reprezintă o unitate funcțională (chiar dacă lucrați cu mai multe entități)
- Plural pentru colecții de unități independente
- În caz de incertitudine sau dacă nu doriți să vă gândiți la asta, alegeți singular
Director public www/
Acest director este singurul accesibil de pe web (așa-numitul document-root). Este posibil să întâlniți adesea numele
public/
în loc de www/
– este doar o chestiune de convenție și nu afectează funcționalitatea.
Directorul conține:
- Punctul de intrare al aplicației
index.php
.htaccess
fișier cu reguli mod_rewrite (pentru Apache)- Fișiere statice (CSS, JavaScript, imagini)
- Fișiere încărcate
Pentru o securitate adecvată a aplicației, este esențial să aveți un document-root configurat corect.
Nu plasați niciodată folderul node_modules/
în acest director – acesta conține mii de fișiere
care pot fi executabile și nu ar trebui să fie accesibile publicului.
Director de aplicații app/
Acesta este directorul principal cu codul aplicației. Structura de bază:
app/ ├── Core/ ← infrastructura contează ├── Model/ ← logică de afaceri ├── Presentation/ ← prezentatoare și șabloane ├── Tasks/ ← scripturi de comandă └── Bootstrap.php ← clasa bootstrap a aplicației
Bootstrap.php
este clasa de pornire a aplicației care inițializează mediul,
încarcă configurația și creează containerul DI.
Să analizăm acum în detaliu subdirectoarele individuale.
Prezentatori și șabloane
Avem partea de prezentare a aplicației în directorul app/Presentation
. O alternativă este directorul scurt
app/UI
. Acesta este locul pentru toate prezentatoarele, șabloanele lor și orice clase ajutătoare.
Organizăm acest strat pe domenii. Într-un proiect complex care combină e-commerce, blog și API, structura ar arăta astfel:
app/Presentation/ ├── Shop/ ← e-commerce frontend │ ├── Product/ │ ├── Cart/ │ └── Order/ ├── Blog/ ← blog │ ├── Home/ │ └── Post/ ├── Admin/ ← administrare │ ├── Dashboard/ │ └── Products/ └── Api/ ← puncte finale API └── V1/
În schimb, pentru un blog simplu am folosi această structură:
app/Presentation/ ├── Front/ ← website frontend │ ├── Home/ │ └── Post/ ├── Admin/ ← administrare │ ├── Dashboard/ │ └── Posts/ ├── Error/ └── Export/ ← RSS, sitemaps etc.
Dosarele precum Home/
sau Dashboard/
conțin prezentatori și modele. Dosarele precum
Front/
, Admin/
sau Api/
se numesc module. Din punct de vedere tehnic, acestea sunt
directoare obișnuite care servesc la organizarea logică a aplicației.
Fiecare folder cu un prezentator conține un prezentator cu nume similar și șabloanele sale. De exemplu, folderul
Dashboard/
conține:
Dashboard/ ├── DashboardPresenter.php ← prezentator └── default.latte ← șablon
Această structură de directoare este reflectată în spațiile de nume ale claselor. De exemplu,
DashboardPresenter
este în spațiul de nume App\Presentation\Admin\Dashboard
(a se vedea maparea prezentatorului):
namespace App\Presentation\Admin\Dashboard;
class DashboardPresenter extends Nette\Application\UI\Presenter
{
//...
}
Ne referim la prezentatorul Dashboard
din modulul Admin
al aplicației folosind notația două puncte
ca Admin:Dashboard
. La acțiunea sa default
atunci ca Admin:Dashboard:default
. Pentru
modulele imbricate, folosim mai multe două puncte, de exemplu Shop:Order:Detail:default
.
Dezvoltarea unei structuri flexibile
Unul dintre marile avantaje ale acestei structuri este cât de elegant se adaptează la nevoile în creștere ale proiectului. Ca exemplu, să luăm partea de generare a fluxurilor XML. Inițial, avem un formular simplu:
Export/ ├── ExportPresenter.php ← un singur prezentator pentru toate exporturile ├── sitemap.latte ← șablon pentru sitemap └── feed.latte ← șablon pentru RSS feed
Cu timpul, se adaugă mai multe tipuri de feed-uri și avem nevoie de mai multă logică pentru ele… Nicio problemă! Dosarul
Export/
devine pur și simplu un modul:
Export/ ├── Sitemap/ │ ├── SitemapPresenter.php │ └── sitemap.latte └── Feed/ ├── FeedPresenter.php ├── amazon.latte ← feed pentru Amazon └── ebay.latte ← feed pentru eBay
Această transformare este complet lină – trebuie doar să creați noi subfoldere, să împărțiți codul în ele și să
actualizați legăturile (de exemplu, de la Export:feed
la Export:Feed:amazon
). Datorită acestui fapt,
putem extinde treptat structura după cum este necesar, nivelul de anidare nu este limitat în niciun fel.
De exemplu, dacă în administrare aveți multe prezentatoare legate de gestionarea comenzilor, cum ar fi
OrderDetail
, OrderEdit
, OrderDispatch
etc., puteți crea un modul (folder)
Order
pentru o mai bună organizare, care va conține (foldere pentru) prezentatoarele Detail
,
Edit
, Dispatch
și altele.
Locația șablonului
În exemplele anterioare, am văzut că șabloanele sunt localizate direct în folderul cu prezentatorul:
Dashboard/ ├── DashboardPresenter.php ← prezentator ├── DashboardTemplate.php ← clasă șablon opțională └── default.latte ← șablon
Această locație se dovedește a fi cea mai convenabilă în practică – aveți toate fișierele aferente chiar la îndemână.
Alternativ, puteți plasa șabloanele într-un subfolder templates/
. Nette acceptă ambele variante. Puteți chiar
plasa șabloanele complet în afara folderului Presentation/
. Totul despre opțiunile de amplasare a șabloanelor
poate fi găsit în capitolul Căutare șabloane.
Clase ajutătoare și componente
Prezentatoarele și șabloanele vin adesea cu alte fișiere ajutătoare. Le plasăm logic în funcție de domeniul lor de aplicare:
1. Direct cu prezentatorul în cazul componentelor specifice pentru prezentatorul dat:
Product/ ├── ProductPresenter.php ├── ProductGrid.php ← componentă pentru listarea produselor └── FilterForm.php ← formular pentru filtrare
2. Pentru modul – se recomandă utilizarea folderului Accessory
, care este plasat ordonat la începutul
alfabetului:
Front/ ├── Accessory/ │ ├── NavbarControl.php ← componente pentru frontend │ └── TemplateFilters.php ├── Product/ └── Cart/
3. Pentru întreaga aplicație – în Presentation/Accessory/
:
app/Presentation/ ├── Accessory/ │ ├── LatteExtension.php │ └── TemplateFilters.php ├── Front/ └── Admin/
Sau puteți plasa clase ajutătoare precum LatteExtension.php
sau TemplateFilters.php
în folderul de
infrastructură app/Core/Latte/
. Iar componentele în app/Components
. Alegerea depinde de convențiile
echipei.
Model – inima aplicației
Modelul conține toată logica de afaceri a aplicației. Pentru organizarea sa, se aplică aceeași regulă – structurăm pe domenii:
app/Model/ ├── Payment/ ← totul despre plăți │ ├── PaymentFacade.php ← punctul principal de intrare │ ├── PaymentRepository.php │ ├── Payment.php ← entitate ├── Order/ ← totul despre comenzi │ ├── OrderFacade.php │ ├── OrderRepository.php │ ├── Order.php └── Shipping/ ← totul despre expediere
În model, întâlniți de obicei aceste tipuri de clase:
Facade: reprezintă principalul punct de intrare într-un anumit domeniu al aplicației. Ele acționează ca un orchestrator care coordonează cooperarea între diferite servicii pentru a implementa cazuri de utilizare complete (cum ar fi „crearea comenzii“ sau „procesarea plății“). Sub stratul lor de orchestrare, fațada ascunde detaliile de implementare de restul aplicației, oferind astfel o interfață curată pentru lucrul cu domeniul dat.
class OrderFacade
{
public function createOrder(Cart $cart): Order
{
// validare
// crearea comenzii
// trimiterea de e-mailuri
// scrierea în statistici
}
}
Servicii: se concentrează pe operațiuni comerciale specifice în cadrul unui domeniu. Spre deosebire de facade care orchestrează cazuri de utilizare întregi, un serviciu implementează o logică de afaceri specifică (cum ar fi calcularea prețurilor sau procesarea plăților). Serviciile sunt de obicei fără stare și pot fi utilizate fie de facade ca elemente de bază pentru operațiuni mai complexe, fie direct de alte părți ale aplicației pentru sarcini mai simple.
class PricingService
{
public function calculateTotal(Order $order): Money
{
// calcularea prețului
}
}
Repositories: gestionează toate comunicațiile cu spațiul de stocare a datelor, de obicei o bază de date. Sarcina lor este să încarce și să salveze entități și să implementeze metode de căutare a acestora. Un depozit protejează restul aplicației de detaliile implementării bazei de date și oferă o interfață orientată pe obiect pentru lucrul cu datele.
class OrderRepository
{
public function find(int $id): ?Order
{
}
public function findByCustomer(int $customerId): array
{
}
}
Entități: obiecte care reprezintă principalele concepte de afaceri din aplicație, care au identitatea lor și se modifică în timp. De obicei, acestea sunt clase mapate la tabelele bazei de date utilizând ORM (precum Nette Database Explorer sau Doctrine). Entitățile pot conține reguli de afaceri privind datele lor și logica de validare.
// Entitate asociată cu tabelul din baza de date comenzi
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,
]);
}
}
Obiecte valoare: obiecte imuabile care reprezintă valori fără identitate proprie – de exemplu, o sumă de bani sau o adresă de e-mail. Două instanțe ale unui obiect valoare cu aceleași valori sunt considerate identice.
Codul infrastructurii
Dosarul Core/
(sau, de asemenea, Infrastructure/
) găzduiește baza tehnică a aplicației. Codul de
infrastructură include de obicei:
app/Core/ ├── Router/ ← gestionarea rutelor și a URL-urilor │ └── RouterFactory.php ├── Security/ ← autentificare și autorizare │ ├── Authenticator.php │ └── Authorizator.php ├── Logging/ ← logare și monitorizare │ ├── SentryLogger.php │ └── FileLogger.php ├── Cache/ ← strat de caching │ └── FullPageCache.php └── Integration/ ← integrarea cu servicii ext. ├── Slack/ └── Stripe/
Pentru proiectele mai mici, o structură plată este în mod natural suficientă:
Core/ ├── RouterFactory.php ├── Authenticator.php └── QueueMailer.php
Acesta este codul care:
- Gestionează infrastructura tehnică (rutare, logare, caching)
- integrează servicii externe (Sentry, Elasticsearch, Redis)
- furnizează servicii de bază pentru întreaga aplicație (e-mail, bază de date)
- Este în mare parte independent de domeniul specific – cache-ul sau logger-ul funcționează la fel pentru e-commerce sau blog.
Vă întrebați dacă o anumită clasă își are locul aici sau în model? Diferența esențială este că codul din
Core/
:
- Nu știe nimic despre domeniu (produse, comenzi, articole)
- poate fi de obicei transferat într-un alt proiect
- Rezolvă „cum funcționează“ (cum să trimiteți e-mail), nu „ce face“ (ce e-mail să trimiteți)
Exemplu pentru o mai bună înțelegere:
App\Core\MailerFactory
– creează instanțe ale clasei de trimitere a e-mailurilor, gestionează setările SMTPApp\Model\OrderMailer
– utilizeazăMailerFactory
pentru a trimite e-mailuri despre comenzi, cunoaște modelele acestora și când ar trebui trimise
Scripturi de comandă
Aplicațiile trebuie adesea să execute sarcini în afara cererilor HTTP obișnuite – fie că este vorba de procesarea
datelor în fundal, întreținere sau sarcini periodice. Scripturile simple din directorul bin/
sunt utilizate pentru
execuție, în timp ce logica de implementare efectivă este plasată în app/Tasks/
(sau
app/Commands/
).
Exemplu:
app/Tasks/ ├── Maintenance/ ← scripturi de întreținere │ ├── CleanupCommand.php ← ștergerea datelor vechi │ └── DbOptimizeCommand.php ← optimizarea bazei de date ├── Integration/ ← integrarea cu sisteme externe │ ├── ImportProducts.php ← import din sistemul furnizorului │ └── SyncOrders.php ← sincronizarea comenzilor └── Scheduled/ ← sarcini regulate ├── NewsletterCommand.php ← trimiterea de buletine informative └── ReminderCommand.php ← notificări pentru clienți
Ce face parte din model și ce din scripturile de comandă? De exemplu, logica pentru trimiterea unui e-mail face parte din
model, trimiterea în masă a mii de e-mailuri face parte din Tasks/
.
Sarcinile sunt de obicei executate din linia de
comandă sau prin cron. Ele pot fi, de asemenea, executate prin intermediul unei cereri HTTP, dar trebuie să se țină cont
de securitate. Prezentatorul care execută sarcina trebuie să fie securizat, de exemplu numai pentru utilizatorii conectați sau
cu un token puternic și acces de la adresele IP permise. Pentru sarcinile lungi, este necesar să creșteți limita de timp a
scriptului și să utilizați session_write_close()
pentru a evita blocarea sesiunii.
Alte directoare posibile
În plus față de directoarele de bază menționate, puteți adăuga alte foldere specializate în funcție de nevoile proiectului. Să ne uităm la cele mai comune și la utilizarea lor:
app/ ├── Api/ ← Logica API independentă de stratul de prezentare ├── Database/ ← scripturi de migrare și seederi pentru datele de testare ├── Components/ ← componente vizuale partajate în întreaga aplicație ├── Event/ ← utile dacă se utilizează o arhitectură bazată pe evenimente ├── Mail/ ← șabloane de e-mail și logica aferentă └── Utils/ ← clase ajutătoare
Pentru componentele vizuale partajate utilizate în prezentatoare în întreaga aplicație, puteți utiliza folderul
app/Components
sau app/Controls
:
app/Components/ ├── Form/ ← componente partajate pentru formulare │ ├── SignInForm.php │ └── UserForm.php ├── Grid/ ← componente pentru liste de date │ └── DataGrid.php └── Navigation/ ← elemente de navigare ├── Breadcrumbs.php └── Menu.php
Acesta este locul componentelor cu logică mai complexă. Dacă doriți să partajați componente între mai multe proiecte, este bine să le separați într-un pachet compozitor de sine stătător.
În directorul app/Mail
puteți plasa gestionarea comunicării prin e-mail:
app/Mail/ ├── templates/ ← șabloane de e-mail │ ├── order-confirmation.latte │ └── welcome.latte └── OrderMailer.php
Prezentator Mapping
Maparea definește reguli pentru derivarea numelor de clase din numele prezentatorilor. Le specificăm în configurare sub cheia application › mapping
.
Pe această pagină, am arătat că plasăm prezentatorii în folderul app/Presentation
(sau app/UI
).
Trebuie să îi spunem lui Nette despre această convenție în fișierul de configurare. Un singur rând este suficient:
application:
mapping: App\Presentation\*\**Presenter
Cum funcționează maparea? Pentru a înțelege mai bine, să ne imaginăm mai întâi o aplicație fără module. Dorim ca
clasele prezentatorului să se încadreze în spațiul de nume App\Presentation
, astfel încât prezentatorul
Home
să se mapeze la clasa App\Presentation\HomePresenter
. Acest lucru se realizează cu această
configurație:
application:
mapping: App\Presentation\*Presenter
Maparea funcționează prin înlocuirea asteriscului din masca App\Presentation\*Presenter
cu numele
prezentatorului Home
, rezultând numele final al clasei App\Presentation\HomePresenter
. Simplu!
Cu toate acestea, după cum vedeți în exemplele din acest capitol și din alte capitole, plasăm clasele prezentatorului în
subdirectoare eponime, de exemplu prezentatorul Home
se mapează la clasa
App\Presentation\Home\HomePresenter
. Obținem acest lucru prin dublarea două puncte (necesită Nette
Application 3.2):
application:
mapping: App\Presentation\**Presenter
Acum vom trece la maparea prezentatorilor în module. Putem defini maparea specifică pentru fiecare modul:
application:
mapping:
Front: App\Presentation\Front\**Presenter
Admin: App\Presentation\Admin\**Presenter
Api: App\Api\*Presenter
Conform acestei configurații, prezentatorul Front:Home
se mapează la clasa
App\Presentation\Front\Home\HomePresenter
, în timp ce prezentatorul Api:OAuth
se mapează la clasa
App\Api\OAuthPresenter
.
Deoarece modulele Front
și Admin
au o metodă de corespondență similară și probabil vor exista
mai multe astfel de module, este posibil să se creeze o regulă generală care să le înlocuiască. Un nou asterisc pentru
modul va fi adăugat la masca clasei:
application:
mapping:
*: App\Presentation\*\**Presenter
Api: App\Api\*Presenter
Aceasta funcționează, de asemenea, pentru structuri de directoare imbricate mai adânc, cum ar fi prezentatorul
Admin:User:Edit
, unde segmentul cu asterisc se repetă pentru fiecare nivel și are ca rezultat clasa
App\Presentation\Admin\User\Edit\EditPresenter
.
O notație alternativă este utilizarea unui array format din trei segmente în loc de un șir. Această notație este echivalentă cu cea anterioară:
application:
mapping:
*: [App\Presentation, *, **Presenter]
Api: [App\Api, '', *Presenter]