Struktura imenika aplikacije
Kako zasnovati jasno in razširljivo imeniško strukturo za projekte v okolju Nette Framework? Predstavili vam bomo preizkušene prakse, ki vam bodo pomagale organizirati kodo. Naučili se boste:
- kako logično strukturirati aplikacijo v imenike
- kako zasnovati strukturo, da se bo z rastjo projekta dobro razširila
- katere so možne alternative in njihove prednosti ali slabosti
Pomembno je omeniti, da samo ogrodje Nette Framework ne vztraja pri nobeni posebni strukturi. Zasnovan je tako, da ga je mogoče zlahka prilagoditi vsem potrebam in željam.
Osnovna struktura projekta
Čeprav ogrodje Nette Framework ne narekuje nobene fiksne strukture imenikov, obstaja preverjena privzeta ureditev v obliki spletnega projekta:
web-project/ ├── app/ ← imenik aplikacij ├── assets/ ← SCSS, JS datoteke, slike..., lahko tudi resources/ ├── bin/ ← skripte ukazne vrstice ├── config/ ← konfiguracija ├── log/ ← zabeležene napake ├── temp/ ← začasne datoteke, predpomnilnik ├── tests/ ← testi ├── vendor/ ← knjižnice, ki jih je namestil Composer └── www/ ← javni imenik (document-root)
To strukturo lahko poljubno spreminjate glede na svoje potrebe – preimenujete ali premikate mape. Nato morate le prilagoditi
relativne poti do imenikov v Bootstrap.php
in po možnosti composer.json
. Nič drugega ni potrebno,
nobene zapletene ponovne konfiguracije, nobenih stalnih sprememb. Nette ima pametno samodejno zaznavanje in samodejno prepozna
lokacijo aplikacije, vključno z njeno bazo URL.
Načela organizacije kode
Ko prvič raziskujete nov projekt, se morate znati hitro orientirati. Predstavljajte si, da kliknete na imenik
app/Model/
in vidite to strukturo:
app/Model/ ├── Services/ ├── Repositories/ └── Entities/
Iz nje boste izvedeli le, da projekt uporablja nekatere storitve, skladišča in entitete. Ne boste izvedeli ničesar o dejanskem namenu aplikacije.
Oglejmo si drugačen pristop – organizacija po domenah:
app/Model/ ├── Cart/ ├── Payment/ ├── Order/ └── Product/
Na prvi pogled je jasno, da gre za spletno mesto e-trgovine. Že imena imenikov razkrivajo, kaj aplikacija zmore – dela s plačili, naročili in izdelki.
Prvi pristop (organizacija po vrsti razreda) v praksi prinaša več težav: koda, ki je logično povezana, je razpršena po različnih mapah in med njimi je treba preskakovati. Zato se bomo organizirali po domenah.
Prostori imen
Običajno struktura imenikov ustreza imenskim prostorom v aplikaciji. To pomeni, da fizična lokacija datotek ustreza
njihovemu imenskemu prostoru. Na primer, razred, ki se nahaja v app/Model/Product/ProductRepository.php
, mora imeti
imenski prostor App\Model\Product
. To načelo pomaga pri usmerjanju kode in poenostavlja samodejno nalaganje.
Ednina in množina v imenih
Opazite, da za glavne imenike aplikacij uporabljamo ednino: app
config
, log
,
temp
, www
. Enako velja tudi znotraj aplikacije: Model
, Core
,
Presentation
. To je zato, ker vsak od njih predstavlja en enoten koncept.
Podobno tudi app/Model/Product
predstavlja vse o izdelkih. Ne imenujemo je Products
, ker to ni mapa,
polna izdelkov (ki bi vsebovala datoteke, kot so iphone.php
, samsung.php
). To je imenski prostor, ki
vsebuje razrede za delo z izdelki – ProductRepository.php
, ProductService.php
.
Mapa app/Tasks
je množinska, ker vsebuje nabor samostojnih izvršilnih skript – CleanupTask.php
,
ImportTask.php
. Vsaka od njih je samostojna enota.
Zaradi doslednosti priporočamo uporabo:
- ednino za imenske prostore, ki predstavljajo funkcionalno enoto (tudi če delate z več enotami)
- množino za zbirke neodvisnih enot
- V primeru negotovosti ali če o tem ne želite razmišljati, izberite ednino
Javni imenik www/
Ta imenik je edini, ki je dostopen s spleta (tako imenovani koren dokumentov). Pogosto lahko namesto imena www/
naletite na ime public/
– to je le stvar konvencije in ne vpliva na funkcionalnost. Imenik vsebuje:
- vstopno točko aplikacije
index.php
- datoteko
.htaccess
s pravili mod_rewrite (za Apache) - statične datoteke (CSS, JavaScript, slike)
- naložene datoteke
Za ustrezno varnost aplikacije je ključnega pomena, da je pravilno konfiguriran document-root.
V ta imenik nikoli ne postavite mape node_modules/
– vsebuje na tisoče datotek, ki so lahko
izvedljive in ne smejo biti javno dostopne.
Imenik aplikacij app/
To je glavni imenik z aplikacijsko kodo. Osnovna struktura:
app/ ├── Core/ ← infrastrukturne zadeve ├── Model/ ← poslovna logika ├── Presentation/ ← predstavitve in predloge ├── Tasks/ ← skripte ukazov └── Bootstrap.php ← zagonski razred aplikacije
Bootstrap.php
je zagonski razred aplikacije, ki inicializira okolje, naloži
konfiguracijo in ustvari vsebnik DI.
Zdaj si podrobno oglejmo posamezne podimenike.
Predstavniki in predloge
Predstavitveni del aplikacije imamo v imeniku app/Presentation
. Druga možnost je kratek naslov
app/UI
. To je prostor za vse predstavitve, njihove predloge in morebitne pomožne razrede.
To plast organiziramo po domenah. V kompleksnem projektu, ki združuje e-trgovino, blog in API, bi bila struktura videti takole:
app/Presentation/ ├── Shop/ ← e-trgovina frontend │ ├── Product/ │ ├── Cart/ │ └── Order/ ├── Blog/ ← blog │ ├── Home/ │ └── Post/ ├── Admin/ ← uprava │ ├── Dashboard/ │ └── Products/ └── Api/ ← Končne točke API └── V1/
Za preprost blog pa bi uporabili to strukturo:
app/Presentation/ ├── Front/ ← spletna stran frontend │ ├── Home/ │ └── Post/ ├── Admin/ ← uprava │ ├── Dashboard/ │ └── Posts/ ├── Error/ └── Export/ ← RSS, zemljevide itd.
V mapah, kot sta Home/
ali Dashboard/
, so predstavniki in predloge. Mape, kot so
Front/
, Admin/
ali Api/
, se imenujejo moduli. Tehnično gledano so to običajni
imeniki, ki služijo za logično organizacijo aplikacije.
Vsaka mapa s predstavnikom vsebuje podobno poimenovane predstavnike in njihove predloge. Na primer, mapa
Dashboard/
vsebuje:
Dashboard/ ├── DashboardPresenter.php ← voditelj └── default.latte ← predloga
Ta struktura imenikov se odraža v imenskih prostorih razredov. Na primer, DashboardPresenter
je v imenskem
prostoru App\Presentation\Admin\Dashboard
(glejte preslikavo predavateljev):
namespace App\Presentation\Admin\Dashboard;
class DashboardPresenter extends Nette\Application\UI\Presenter
{
//...
}
Predstavnik Dashboard
znotraj modula Admin
v aplikaciji označujemo z zapisom v dvopičju kot
Admin:Dashboard
. Na njegovo akcijo default
pa nato kot Admin:Dashboard:default
. Za
vgnezdene module uporabljamo več dvopičij, na primer Shop:Order:Detail:default
.
Razvoj prilagodljive strukture
Ena od velikih prednosti te strukture je, kako elegantno se prilagaja naraščajočim potrebam projekta. Kot primer vzemimo del, ki ustvarja vire XML. Na začetku imamo preprost obrazec:
Export/ ├── ExportPresenter.php ← en predavatelj za ves izvoz ├── sitemap.latte ← predlogo za karto spletnega mesta └── feed.latte ← predlogo za vir RSS
Sčasoma se doda več vrst virov in zanje potrebujemo več logike… Ni problema! Mapa Export/
preprosto
postane modul:
Export/ ├── Sitemap/ │ ├── SitemapPresenter.php │ └── sitemap.latte └── Feed/ ├── FeedPresenter.php ├── amazon.latte ← krma za Amazon └── ebay.latte ← vir za eBay
Preoblikovanje je popolnoma nemoteno – ustvarite nove podmape, razdelite kodo vanje in posodobite povezave (npr. iz
Export:feed
v Export:Feed:amazon
). Zaradi tega lahko strukturo postopoma širimo po potrebi, raven
gnezdenja ni v ničemer omejena.
Če imate na primer v administraciji veliko predstavnikov, povezanih z upravljanjem naročil, kot so
OrderDetail
, OrderEdit
, OrderDispatch
itd, lahko za boljšo organizacijo ustvarite modul
(mapo) Order
, ki bo vseboval (mape za) predstavnike Detail
, Edit
, Dispatch
in druge.
Lokacija predloge
V prejšnjih primerih smo videli, da se predloge nahajajo neposredno v mapi s predstavitvijo:
Dashboard/ ├── DashboardPresenter.php ← voditelj ├── DashboardTemplate.php ← neobvezen razred predloge └── default.latte ← Predloga
Ta lokacija se v praksi izkaže za najprimernejšo – vse povezane datoteke imate takoj pri roki.
Predloge lahko namestite tudi v podmapo templates/
. Nette podpira obe različici. Predloge lahko postavite tudi
povsem zunaj mape Presentation/
. Vse o možnostih lokacije predlog najdete v poglavju Iskanje predlog.
Pomožni razredi in komponente
Predstavniki in predloge so pogosto opremljeni z drugimi pomožnimi datotekami. Razporedimo jih logično glede na njihovo področje uporabe:
1. Neposredno s predstavitvijo v primeru posebnih komponent za določeno predstavitev:
Product/ ├── ProductPresenter.php ├── ProductGrid.php ← komponenta za uvrstitev izdelka na seznam └── FilterForm.php ← obrazec za filtriranje
2. Za modul – priporočamo uporabo mape Accessory
, ki je lepo postavljena na začetek abecede:
Front/ ├── Accessory/ │ ├── NavbarControl.php ← komponente za frontend │ └── TemplateFilters.php ├── Product/ └── Cart/
3. Za celotno aplikacijo – v Presentation/Accessory/
:
app/Presentation/ ├── Accessory/ │ ├── LatteExtension.php │ └── TemplateFilters.php ├── Front/ └── Admin/
Lahko pa pomožne razrede, kot sta LatteExtension.php
ali TemplateFilters.php
, namestite
v infrastrukturno mapo app/Core/Latte/
. Komponente pa v mapo app/Components
. Izbira je odvisna od
skupinskih konvencij.
Model – srce aplikacije
Model vsebuje vso poslovno logiko aplikacije. Za njegovo organizacijo velja enako pravilo – strukturiramo ga po domenah:
app/Model/ ├── Payment/ ← vse o plačilih │ ├── PaymentFacade.php ← glavna vstopna točka │ ├── PaymentRepository.php │ ├── Payment.php ← entiteta ├── Order/ ← vse o naročilih │ ├── OrderFacade.php │ ├── OrderRepository.php │ ├── Order.php └── Shipping/ ← vse o odpremi
V modelu se običajno srečamo s temi vrstami razredov:
Fasade: predstavljajo glavno vstopno točko v določeno domeno v aplikaciji. Delujejo kot orkestrator, ki usklajuje sodelovanje med različnimi storitvami za izvajanje celotnih primerov uporabe (kot sta „ustvariti naročilo“ ali „obdelati plačilo“). Pod orkestracijsko plastjo fasada skriva podrobnosti izvajanja pred preostalim delom aplikacije in tako zagotavlja čist vmesnik za delo z določeno domeno.
class OrderFacade
{
public function createOrder(Cart $cart): Order
{
// potrjevanje
// ustvarjanje naročila.
// pošiljanje e-pošte
// pisanje v statistiko
}
}
Službe: osredotočajo se na posebne poslovne operacije znotraj domene. Za razliko od fasad, ki orkestrirajo celotne primere uporabe, storitev izvaja specifično poslovno logiko (kot so izračuni cen ali obdelava plačil). Storitve so običajno brez stanja in jih lahko uporabljajo fasade kot gradnike za kompleksnejše operacije ali drugi deli aplikacije neposredno za enostavnejša opravila.
class PricingService
{
public function calculateTotal(Order $order): Money
{
// izračun cene
}
}
Hrambe: skrbijo za vso komunikacijo s hrambo podatkov, običajno s podatkovno zbirko. Njihova naloga je nalaganje in shranjevanje entitet ter izvajanje metod za njihovo iskanje. Skladišče ščiti preostalo aplikacijo pred podrobnostmi izvajanja podatkovne zbirke in zagotavlja objektno usmerjen vmesnik za delo s podatki.
class OrderRepository
{
public function find(int $id): ?Order
{
}
public function findByCustomer(int $customerId): array
{
}
}
Entitete: objekti, ki predstavljajo glavne poslovne koncepte v aplikaciji, ki imajo svojo identiteto in se s časom spreminjajo. Običajno so to razredi, ki so s pomočjo ORM (kot sta Nette Database Explorer ali Doctrine) preslikani v tabele podatkovne zbirke. Entitete lahko vsebujejo poslovna pravila v zvezi z njihovimi podatki in logiko potrjevanja.
// Entiteta, preslikana v tabelo naročil v zbirki podatkov
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,
]);
}
}
Vrednostni objekti: nespremenljivi objekti, ki predstavljajo vrednosti brez lastne identitete – na primer znesek denarja ali e-poštni naslov. Dva primerka objekta vrednosti z enakimi vrednostmi veljata za enaka.
Koda infrastrukture
V mapi Core/
(ali tudi Infrastructure/
) se nahaja tehnični temelj aplikacije. Infrastrukturna koda
običajno vključuje:
app/Core/ ├── Router/ ← usmerjanje in upravljanje URL. │ └── RouterFactory.php ├── Security/ ← avtentikacija in avtorizacija. │ ├── Authenticator.php │ └── Authorizator.php ├── Logging/ ← beleženje in spremljanje │ ├── SentryLogger.php │ └── FileLogger.php ├── Cache/ ← plast predpomnilnika │ └── FullPageCache.php └── Integration/ ← integracija z zunanjimi storitvami ├── Slack/ └── Stripe/
Za manjše projekte seveda zadostuje ravna struktura:
Core/ ├── RouterFactory.php ├── Authenticator.php └── QueueMailer.php
To je koda, ki:
- Obravnava tehnično infrastrukturo (usmerjanje, beleženje, predpomnilnik).
- vključuje zunanje storitve (Sentry, Elasticsearch, Redis)
- zagotavlja osnovne storitve za celotno aplikacijo (pošta, zbirka podatkov)
- je večinoma neodvisna od določene domene – predpomnilnik ali logger deluje enako za e-trgovino ali blog.
Se sprašujete, ali določen razred spada sem ali v model? Ključna razlika je v tem, da je koda v Core/
:
- ne ve ničesar o domeni (izdelki, naročila, članki)
- Običajno jo je mogoče prenesti v drug projekt
- rešuje „kako deluje“ (kako poslati pošto) in ne „kaj počne“ (kakšno pošto poslati)
Primer za boljše razumevanje:
App\Core\MailerFactory
– ustvari primerke razreda za pošiljanje e-pošte, skrbi za nastavitve SMTPApp\Model\OrderMailer
– uporabljaMailerFactory
za pošiljanje e-poštnih sporočil o naročilih, pozna njihove predloge in ve, kdaj jih je treba poslati
Skripte ukazov
Aplikacije morajo pogosto opravljati naloge zunaj običajnih zahtevkov HTTP – bodisi gre za obdelavo podatkov v ozadju,
vzdrževanje ali periodična opravila. Za izvajanje se uporabljajo preproste skripte v imeniku bin/
, medtem ko je
dejanska izvedbena logika nameščena v imeniku app/Tasks/
(ali app/Commands/
).
Primer:
app/Tasks/ ├── Maintenance/ ← skripte za vzdrževanje │ ├── CleanupCommand.php ← brisanje starih podatkov │ └── DbOptimizeCommand.php ← optimizacija podatkovne zbirke ├── Integration/ ← integracija z zunanjimi sistemi │ ├── ImportProducts.php ← uvoz iz sistema dobavitelja │ └── SyncOrders.php ← sinhronizacija naročil └── Scheduled/ ← redna opravila ├── NewsletterCommand.php ← pošiljanje novic └── ReminderCommand.php ← obveščanje strank
Kaj spada v model in kaj v ukazne skripte? Na primer, logika za pošiljanje enega e-poštnega sporočila je del modela,
množično pošiljanje tisočih e-poštnih sporočil pa spada v Tasks/
.
Opravila se običajno izvajajo iz ukazne vrstice ali
prek programa cron. Lahko se zaženejo tudi prek zahteve HTTP, vendar je pri tem treba upoštevati varnost. Predstavnik, ki izvaja
opravilo, mora biti zavarovan, na primer samo za prijavljene uporabnike ali z močnim žetonom in dostopom z dovoljenih naslovov
IP. Za dolga opravila je treba povečati časovno omejitev skripta in uporabiti spletno stran session_write_close()
,
da se prepreči zaklepanje seje.
Drugi možni imeniki
Poleg omenjenih osnovnih imenikov lahko glede na potrebe projekta dodate tudi druge specializirane mape. Oglejmo si najpogostejše in njihovo uporabo:
app/ ├── Api/ ← Logika API je neodvisna od predstavitvene plasti ├── Database/ ← migracijske skripte in sejalnike za testne podatke. ├── Components/ ← skupne vizualne komponente v aplikaciji ├── Event/ ← uporabno, če uporabljate arhitekturo, ki temelji na dogodkih ├── Mail/ ← e-poštne predloge in povezana logika └── Utils/ ← pomožni razredi
Za skupne vizualne komponente, ki se uporabljajo v predstavitvah v celotni aplikaciji, lahko uporabite mapo
app/Components
ali app/Controls
:
app/Components/ ├── Form/ ← komponente obrazca v skupni rabi │ ├── SignInForm.php │ └── UserForm.php ├── Grid/ ← komponente za izpise podatkov │ └── DataGrid.php └── Navigation/ ← navigacijski elementi ├── Breadcrumbs.php └── Menu.php
V to mapo spadajo komponente z bolj zapleteno logiko. Če želite komponente deliti med več projekti, jih je dobro ločiti v samostojni paket Composer.
V imenik app/Mail
lahko umestite upravljanje komunikacije z elektronsko pošto:
app/Mail/ ├── templates/ ← e-poštne predloge │ ├── order-confirmation.latte │ └── welcome.latte └── OrderMailer.php
Predavatelj Mapiranje
Mapiranje določa pravila za izpeljavo imen razredov iz imen predvajalnikov. Določimo jih v konfiguraciji pod ključem application › mapping
.
Na tej strani smo pokazali, da predstavnike namestimo v mapo app/Presentation
(ali app/UI
).
V konfiguracijski datoteki moramo Nette obvestiti o tej konvenciji. Zadostuje ena vrstica:
application:
mapping: App\Presentation\*\**Presenter
Kako deluje preslikava? Za boljše razumevanje si najprej predstavljajmo aplikacijo brez modulov. Želimo, da razredi
predstavnikov spadajo v imenski prostor App\Presentation
, tako da se predstavnik Home
preslika
v razred App\Presentation\HomePresenter
. To dosežemo s to konfiguracijo:
application:
mapping: App\Presentation\*Presenter
Mapiranje poteka tako, da se zvezdica v maski App\Presentation\*Presenter
zamenja z imenom predstavnika
Home
, s čimer dobimo končno ime razreda App\Presentation\HomePresenter
. Enostavno!
Vendar, kot vidite v primerih v tem in drugih poglavjih, umeščamo predstavitvene razrede v istoimenske podimenike, na
primer predstavitveni razred Home
se preslika v razred App\Presentation\Home\HomePresenter
. To
dosežemo s podvojitvijo dvopičja (zahteva aplikacijo Nette 3.2):
application:
mapping: App\Presentation\**Presenter
Zdaj bomo prešli na preslikavo predstavnikov v module. Za vsak modul lahko določimo posebno kartiranje:
application:
mapping:
Front: App\Presentation\Front\**Presenter
Admin: App\Presentation\Admin\**Presenter
Api: App\Api\*Presenter
V skladu s to konfiguracijo je predstavnik Front:Home
preslikan v razred
App\Presentation\Front\Home\HomePresenter
, medtem ko je predstavnik Api:OAuth
preslikan v razred
App\Api\OAuthPresenter
.
Ker imata modula Front
in Admin
podoben način preslikave in ker bo takih modulov verjetno več, je
mogoče ustvariti splošno pravilo, ki jih bo nadomestilo. V masko razreda bo dodana nova zvezdica za modul:
application:
mapping:
*: App\Presentation\*\**Presenter
Api: App\Api\*Presenter
Deluje tudi za globlje ugnezdene imeniške strukture, kot je predstavnik Admin:User:Edit
, kjer se segment
z zvezdico ponovi za vsako raven in ima za rezultat razred App\Presentation\Admin\User\Edit\EditPresenter
.
Alternativni zapis je, da namesto niza uporabimo polje, sestavljeno iz treh segmentov. Ta zapis je enakovreden prejšnjemu:
application:
mapping:
*: [App\Presentation, *, **Presenter]
Api: [App\Api, '', *Presenter]