Structura directoarelor aplicației
Cum să proiectăm o structură de directoare clară și scalabilă pentru proiectele în Nette Framework? Vom arăta practici dovedite care vă vor ajuta cu organizarea codului. Veți afla:
- cum să împărțiți logic aplicația în directoare
- cum să proiectați structura astfel încât să scaleze bine odată cu creșterea proiectului
- care sunt alternativele posibile și avantajele sau dezavantajele lor
Este important de menționat că Nette Framework însuși nu impune nicio structură specifică. Este proiectat astfel încât să poată fi ușor adaptat la orice nevoi și preferințe.
Structura de bază a proiectului
Deși Nette Framework nu dictează nicio structură de directoare fixă, există o aranjare implicită dovedită sub forma Web Project:
web-project/ ├── app/ ← director cu aplicația ├── assets/ ← fișiere SCSS, JS, imagini..., alternativ resources/ ├── bin/ ← scripturi pentru linia de comandă ├── config/ ← configurație ├── log/ ← erori înregistrate ├── temp/ ← fișiere temporare, cache ├── tests/ ← teste ├── vendor/ ← biblioteci instalate de Composer └── www/ ← director public (document-root)
Această structură poate fi modificată liber în funcție de nevoile dvs. – folderele pot fi redenumite sau mutate. Apoi
este suficient doar să modificați căile relative către directoare în fișierul Bootstrap.php
și eventual
composer.json
. Nimic mai mult nu este necesar, nicio reconfigurare complicată, nicio modificare a constantelor.
Nette dispune de autodetecție inteligentă și recunoaște automat locația aplicației, inclusiv baza sa URL.
Principii de organizare a codului
Când explorați pentru prima dată un proiect nou, ar trebui să vă orientați rapid în el. Imaginați-vă că deschideți
directorul app/Model/
și vedeți această structură:
app/Model/ ├── Services/ ├── Repositories/ └── Entities/
Din aceasta deduceți doar că proiectul folosește niște servicii, depozite și entități. Despre scopul real al aplicației nu aflați absolut nimic.
Să ne uităm la o altă abordare – organizarea pe domenii:
app/Model/ ├── Cart/ ├── Payment/ ├── Order/ └── Product/
Aici este altfel – la prima vedere este clar că este vorba despre un magazin online. Chiar și numele directoarelor dezvăluie ce poate face aplicația – lucrează cu plăți, comenzi și produse.
Prima abordare (organizarea după tipul claselor) aduce în practică o serie de probleme: codul care este logic legat este fragmentat în diferite foldere și trebuie să săriți între ele. De aceea, vom organiza pe domenii.
Spații de nume
Este obișnuit ca structura directoarelor să corespundă spațiilor de nume din aplicație. Aceasta înseamnă că locația
fizică a fișierelor corespunde namespace-ului lor. De exemplu, o clasă situată în
app/Model/Product/ProductRepository.php
ar trebui să aibă namespace-ul App\Model\Product
. Acest
principiu ajută la orientarea în cod și simplifică autoloading-ul.
Singular vs plural în nume
Observați că pentru directoarele principale ale aplicației folosim singularul: app
, config
,
log
, temp
, www
. La fel și în interiorul aplicației: Model
,
Core
, Presentation
. Acest lucru se datorează faptului că fiecare dintre ele reprezintă un concept
unitar.
Similar, de exemplu, app/Model/Product
reprezintă totul legat de produse. Nu îl vom numi Products
,
deoarece nu este un folder plin de produse (acolo ar fi fișiere nokia.php
, samsung.php
). Este un
namespace care conține clase pentru lucrul cu produse – ProductRepository.php
,
ProductService.php
.
Folderul app/Tasks
este la plural deoarece conține un set de scripturi executabile separate –
CleanupTask.php
, ImportTask.php
. Fiecare dintre ele este o unitate separată.
Pentru consistență, recomandăm utilizarea:
- Singularului pentru namespace-ul care reprezintă un ansamblu funcțional (chiar dacă lucrează cu mai multe entități)
- Pluralului pentru colecții de unități separate
- În caz de incertitudine sau dacă nu doriți să vă gândiți la asta, alegeți singularul
Director public www/
Acest director este singurul accesibil de pe web (așa-numitul document-root). Adesea puteți întâlni și numele
public/
în loc de www/
– este doar o chestiune de convenție și nu are nicio influență asupra
funcționalității aplicației. Directorul conține:
- Punctul de intrare al aplicației
index.php
- Fișierul
.htaccess
cu reguli pentru mod_rewrite (pentru Apache) - Fișiere statice (CSS, JavaScript, imagini)
- Fișiere încărcate
Pentru securitatea corectă a aplicației, este esențial să aveți configurat corect document-root.
Nu plasați niciodată folderul node_modules/
în acest director – conține mii de fișiere care
pot fi executabile și nu ar trebui să fie accesibile public.
Director aplicație app/
Acesta este directorul principal cu codul aplicației. Structura de bază:
app/ ├── Core/ ← aspecte de infrastructură ├── Model/ ← logica de business ├── Presentation/ ← presentere și șabloane ├── Tasks/ ← scripturi de comandă └── Bootstrap.php ← clasa de inițializare a aplicației
Bootstrap.php
este clasa de pornire a aplicației,
care inițializează mediul, încarcă configurația și creează containerul DI.
Să ne uităm acum mai detaliat la subdirectoarele individuale.
Presentere și șabloane
Partea de prezentare a aplicației o avem în directorul app/Presentation
. O alternativă este scurtul
app/UI
. Este locul pentru toți presenterele, șabloanele lor și eventualele clase ajutătoare.
Acest strat îl organizăm pe domenii. Într-un proiect complex, care combină un magazin online, un blog și un API, structura ar arăta astfel:
app/Presentation/ ├── Shop/ ← frontend magazin online │ ├── Product/ │ ├── Cart/ │ └── Order/ ├── Blog/ ← blog │ ├── Home/ │ └── Post/ ├── Admin/ ← administrare │ ├── Dashboard/ │ └── Products/ └── Api/ ← endpoint-uri API └── V1/
Pe de altă parte, pentru un blog simplu, am folosi o împărțire:
app/Presentation/ ├── Front/ ← frontend web │ ├── Home/ │ └── Post/ ├── Admin/ ← administrare │ ├── Dashboard/ │ └── Posts/ ├── Error/ └── Export/ ← RSS, sitemap-uri etc.
Foldere precum Home/
sau Dashboard/
conțin presentere și șabloane. Foldere precum
Front/
, Admin/
sau Api/
le numim module. Tehnic, sunt directoare obișnuite care
servesc la împărțirea logică a aplicației.
Fiecare folder cu un presenter conține un presenter cu același nume și șabloanele sale. De exemplu, folderul
Dashboard/
conține:
Dashboard/ ├── DashboardPresenter.php ← presenter └── default.latte ← șablon
Această structură de directoare se reflectă în spațiile de nume ale claselor. De exemplu, DashboardPresenter
se află în spațiul de nume App\Presentation\Admin\Dashboard
(vezi mapování
presenterů):
namespace App\Presentation\Admin\Dashboard;
class DashboardPresenter extends Nette\Application\UI\Presenter
{
// ...
}
La presenterul Dashboard
din interiorul modulului Admin
facem referire în aplicație folosind
notația cu două puncte ca Admin:Dashboard
. La acțiunea sa default
apoi ca
Admin:Dashboard:default
. În cazul modulelor imbricate, folosim mai multe două puncte, de exemplu
Shop:Order:Detail:default
.
Dezvoltare flexibilă a structurii
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 care generează feed-uri XML. La început avem o formă simplă:
Export/ ├── ExportPresenter.php ← un singur presenter pentru toate exporturile ├── sitemap.latte ← șablon pentru sitemap └── feed.latte ← șablon pentru feed RSS
Cu timpul, apar noi tipuri de feed-uri și avem nevoie de mai multă logică pentru ele… Nicio problemă! Folderul
Export/
devine pur și simplu un modul:
Export/ ├── Sitemap/ │ ├── SitemapPresenter.php │ └── sitemap.latte └── Feed/ ├── FeedPresenter.php ├── zbozi.latte ← feed pentru Zboží.cz └── heureka.latte ← feed pentru Heureka.cz
Această transformare este complet fluidă – este suficient să creați noi subfoldere, să împărțiți codul în ele și
să actualizați linkurile (de ex. de la Export:feed
la Export:Feed:zbozi
). Datorită acestui fapt,
putem extinde treptat structura după necesități, nivelul de imbricare nu este limitat în niciun fel.
Dacă, de exemplu, în administrare aveți mulți presenteri referitori la gestionarea comenzilor, cum ar fi
OrderDetail
, OrderEdit
, OrderDispatch
etc., puteți crea pentru o mai bună organizare în
acest loc un modul (folder) Order
, în care vor fi (foldere pentru) presenterele Detail
,
Edit
, Dispatch
și altele.
Amplasarea șabloanelor
În exemplele anterioare am văzut că șabloanele sunt plasate direct în folderul cu presenterul:
Dashboard/ ├── DashboardPresenter.php ← presenter ├── DashboardTemplate.php ← clasă opțională pentru șablon └── default.latte ← șablon
Această amplasare se dovedește în practică a fi cea mai convenabilă – aveți toate fișierele aferente la îndemână.
Alternativ, puteți plasa șabloanele într-un subfolder templates/
. Nette suportă ambele variante. Puteți chiar
plasa șabloanele complet în afara folderului Presentation/
. Totul despre posibilitățile de amplasare a
șabloanelor găsiți în capitolul Căutarea
șabloanelor.
Clase ajutătoare și componente
Presenterelor și șabloanelor le aparțin adesea și alte fișiere ajutătoare. Le plasăm logic în funcție de domeniul lor de aplicare:
1. Direct lângă presenter în cazul componentelor specifice pentru presenterul respectiv:
Product/ ├── ProductPresenter.php ├── ProductGrid.php ← componentă pentru listarea produselor └── FilterForm.php ← formular pentru filtrare
2. Pentru modul – recomandăm utilizarea folderului Accessory
, care se plasează convenabil chiar 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/
. Și componentele în app/Components
. Alegerea depinde de obiceiurile
echipei.
Model – inima aplicației
Modelul conține întreaga logică de business a aplicației. Pentru organizarea sa se aplică din nou regula – structurăm pe domenii:
app/Model/ ├── Payment/ ← totul despre plăți │ ├── PaymentFacade.php ← principalul punct de intrare │ ├── PaymentRepository.php │ ├── Payment.php ← entitate ├── Order/ ← totul despre comenzi │ ├── OrderFacade.php │ ├── OrderRepository.php │ ├── Order.php └── Shipping/ ← totul despre transport
În model veți întâlni de obicei aceste tipuri de clase:
Facade: reprezintă principalul punct de intrare într-un domeniu specific al aplicației. Acționează ca un orchestrator care coordonează colaborarea între diferite servicii în scopul implementării cazurilor de utilizare complete (cum ar fi “creează comandă” sau “procesează plată”). Sub stratul său de orchestrator, fațada ascunde detaliile de implementare de restul aplicației, oferind astfel o interfață curată pentru lucrul cu domeniul respectiv.
class OrderFacade
{
public function createOrder(Cart $cart): Order
{
// validare
// creare comandă
// trimitere e-mail
// înregistrare în statistici
}
}
Servicii: se concentrează pe o operațiune specifică de business în cadrul domeniului. Spre deosebire de fațadă, care orchestrează cazuri de utilizare întregi, serviciul implementează o logică de business specifică (cum ar fi calcule de prețuri sau procesarea plăților). Serviciile sunt de obicei fără stare și pot fi utilizate fie de fațade ca blocuri de construcție 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
{
// calcul preț
}
}
Depozite: asigură întreaga comunicare cu stocarea de date, de obicei o bază de date. Sarcina sa este de a încărca și salva entități și de a implementa metode pentru căutarea lor. Depozitul izolează restul aplicației de detaliile de implementare ale bazei de date și oferă o interfață orientată pe obiecte 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 business în aplicație, care au identitatea lor și se schimbă în timp. De obicei, sunt clase mapate pe tabele de baze de date folosind ORM (cum ar fi Nette Database Explorer sau Doctrine). Entitățile pot conține reguli de business referitoare la datele lor și logică de validare.
// Entitate mapată pe tabela de bază de date 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,
]);
}
}
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.
Cod de infrastructură
Folderul Core/
(sau și Infrastructure/
) este casa pentru baza tehnică a aplicației. Codul de
infrastructură include de obicei:
app/Core/ ├── Router/ ← rutare și management URL │ └── RouterFactory.php ├── Security/ ← autentificare și autorizare │ ├── Authenticator.php │ └── Authorizator.php ├── Logging/ ← logare și monitorizare │ ├── SentryLogger.php │ └── FileLogger.php ├── Cache/ ← strat de cache │ └── FullPageCache.php └── Integration/ ← integrare cu servicii ext. ├── Slack/ └── Stripe/
Pentru proiecte mai mici, este suficientă, desigur, o structură plată:
Core/ ├── RouterFactory.php ├── Authenticator.php └── QueueMailer.php
Este vorba despre cod care:
- Rezolvă infrastructura tehnică (rutare, logare, cache)
- Integrează servicii externe (Sentry, Elasticsearch, Redis)
- Oferă servicii de bază pentru întreaga aplicație (mail, bază de date)
- Este în mare parte independent de domeniul specific – cache-ul sau loggerul funcționează la fel pentru magazinul online sau blog.
Ezitați dacă o anumită clasă aparține aici sau în model? Diferența cheie este că codul din Core/
:
- Nu știe nimic despre domeniu (produse, comenzi, articole)
- Este în mare parte posibil să fie transferat într-un alt proiect
- Rezolvă “cum funcționează” (cum se trimite un mail), nu “ce face” (ce mail să trimită)
Exemplu pentru o mai bună înțelegere:
App\Core\MailerFactory
– creează instanțe ale clasei pentru trimiterea e-mailurilor, rezolvă setările SMTPApp\Model\OrderMailer
– foloseșteMailerFactory
pentru a trimite e-mailuri despre comenzi, cunoaște șabloanele lor și știe când trebuie trimise
Scripturi de comandă
Aplicațiile au adesea nevoie să execute activități în afara cererilor HTTP obișnuite – fie că este vorba de
procesarea datelor în fundal, întreținere sau sarcini periodice. Pentru rulare se folosesc scripturi simple în directorul
bin/
, logica de implementare propriu-zisă o plasăm apoi în app/Tasks/
(eventual
app/Commands/
).
Exemplu:
app/Tasks/ ├── Maintenance/ ← scripturi de întreținere │ ├── CleanupCommand.php ← ștergerea datelor vechi │ └── DbOptimizeCommand.php ← optimizarea bazei de date ├── Integration/ ← integrare cu sisteme externe │ ├── ImportProducts.php ← import din sistemul furnizorului │ └── SyncOrders.php ← sincronizarea comenzilor └── Scheduled/ ← sarcini regulate ├── NewsletterCommand.php ← trimiterea newsletterelor └── ReminderCommand.php ← notificări clienți
Ce aparține modelului și ce scripturilor de comandă? De exemplu, logica pentru trimiterea unui singur e-mail face parte din
model, trimiterea în masă a mii de e-mailuri aparține deja Tasks/
.
Sarcinile le rulăm de obicei din linia de comandă sau
prin cron. Pot fi rulate și prin cerere HTTP, dar trebuie să ne gândim la securitate. Presenterul care rulează sarcina trebuie
securizat, de exemplu, doar pentru utilizatorii conectați sau cu un token puternic și acces de la adrese IP permise. Pentru
sarcinile lungi, este necesar să se mărească limita de timp a scriptului și să se folosească
session_write_close()
, pentru a nu bloca sesiunea.
Alte directoare posibile
Pe lângă directoarele de bază menționate, puteți adăuga, în funcție de nevoile proiectului, alte foldere specializate. Să ne uităm la cele mai frecvente dintre ele și la utilizarea lor:
app/ ├── Api/ ← logica pentru API independentă de stratul de prezentare ├── Database/ ← scripturi de migrare și seedere pentru date de test ├── Components/ ← componente vizuale partajate în întreaga aplicație ├── Event/ ← util dacă utilizați arhitectura bazată pe evenimente ├── Mail/ ← șabloane de e-mail și logica aferentă └── Utils/ ← clase ajutătoare
Pentru componentele vizuale partajate utilizate în presentere în întreaga aplicație, se poate folosi folderul
app/Components
sau app/Controls
:
app/Components/ ├── Form/ ← componente de formular partajate │ ├── SignInForm.php │ └── UserForm.php ├── Grid/ ← componente pentru listări de date │ └── DataGrid.php └── Navigation/ ← elemente de navigație ├── Breadcrumbs.php └── Menu.php
Aici aparțin componentele care au o logică mai complexă. Dacă doriți să partajați componente între mai multe proiecte, este recomandabil să le extrageți într-un pachet composer separat.
Î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
Maparea presenterelor
Maparea definește reguli pentru derivarea numelui clasei din numele presenterului. Le specificăm în configurație sub cheia application › mapping
.
Pe această pagină am arătat că plasăm presenterele în folderul app/Presentation
(eventual
app/UI
). Această convenție trebuie să o comunicăm lui Nette în fișierul de configurare. Este suficientă
o singură linie:
application:
mapping: App\Presentation\*\**Presenter
Cum funcționează maparea? Pentru o mai bună înțelegere, să ne imaginăm mai întâi o aplicație fără module. Dorim
ca clasele presenterelor să se încadreze în spațiul de nume App\Presentation
, astfel încât presenterul
Home
să fie mapat pe clasa App\Presentation\HomePresenter
. Ceea ce realizăm cu această
configurație:
application:
mapping: App\Presentation\*Presenter
Maparea funcționează astfel încât numele presenterului Home
înlocuiește asteriscul din masca
App\Presentation\*Presenter
, obținând astfel numele final al clasei App\Presentation\HomePresenter
.
Simplu!
Dar, după cum vedeți în exemplele din acest capitol și din altele, plasăm clasele presenterelor în subdirectoare eponime,
de exemplu, presenterul Home
se mapează pe clasa App\Presentation\Home\HomePresenter
. Acest lucru se
realizează prin dublarea celor două puncte (necesită Nette Application 3.2):
application:
mapping: App\Presentation\**Presenter
Acum trecem la maparea presenterelor în module. Pentru fiecare modul putem defini o mapare specifică:
application:
mapping:
Front: App\Presentation\Front\**Presenter
Admin: App\Presentation\Admin\**Presenter
Api: App\Api\*Presenter
Conform acestei configurații, presenterul Front:Home
se mapează pe clasa
App\Presentation\Front\Home\HomePresenter
, în timp ce presenterul Api:OAuth
pe clasa
App\Api\OAuthPresenter
.
Deoarece modulele Front
și Admin
au un mod similar de mapare și probabil vor exista mai multe
astfel de module, este posibil să se creeze o regulă generală care să le înlocuiască. Astfel, în masca clasei va apărea
un nou asterisc pentru modul:
application:
mapping:
*: App\Presentation\*\**Presenter
Api: App\Api\*Presenter
Funcționează și pentru structuri de directoare mai adânc imbricate, cum ar fi, de exemplu, presenterul
Admin:User:Edit
, segmentul cu asterisc se repetă pentru fiecare nivel și rezultatul este clasa
App\Presentation\Admin\User\Edit\EditPresenter
.
O notație alternativă este să folosim un array format din trei segmente în loc de un șir de caractere. Această notație este echivalentă cu cea anterioară:
application:
mapping:
*: [App\Presentation, *, **Presenter]
Api: [App\Api, '', *Presenter]