Struktura mape aplikacije
Kako zasnovati pregledno in razširljivo strukturo map za projekte v Nette Framework? Pokazali si bomo preverjene prakse, ki vam bodo pomagale pri organizaciji kode. Izvedeli boste:
- kako logično razčleniti aplikacijo v mape
- kako strukturo zasnovati tako, da dobro skalira z rastjo projekta
- kakšne so možne alternative in njihove prednosti ali slabosti
Pomembno je omeniti, da Nette Framework sam po sebi ne vztraja pri nobeni konkretni strukturi. Zasnovan je tako, da se ga da enostavno prilagoditi kakršnimkoli potrebam in preferencam.
Osnovna struktura projekta
Čeprav Nette Framework ne narekuje nobene fiksne strukture map, obstaja preverjena privzeta ureditev v obliki Web Project:
web-project/ ├── app/ ← mapa z aplikacijo ├── assets/ ← datoteke SCSS, JS, slike..., alternativno resources/ ├── bin/ ← skripti za ukazno vrstico ├── config/ ← konfiguracija ├── log/ ← zabeležene napake ├── temp/ ← začasne datoteke, predpomnilnik ├── tests/ ← testi ├── vendor/ ← knjižnice, nameščene s Composerjem └── www/ ← javna mapa (document-root)
To strukturo lahko poljubno urejate glede na svoje potrebe – mape preimenujete ali premaknete. Nato je dovolj le urediti
relativne poti do map v datoteki Bootstrap.php
in po potrebi composer.json
. Nič več ni potrebno,
nobene zapletene rekonfiguracije, nobenih sprememb konstant. Nette razpolaga s pametnim samodejnim zaznavanjem in samodejno
prepozna lokacijo aplikacije, vključno z njeno osnovno URL.
Principi organizacije kode
Ko prvič raziskujete nov projekt, bi se morali v njem hitro znajti. Predstavljajte si, da odprete mapo
app/Model/
in vidite to strukturo:
app/Model/ ├── Services/ ├── Repositories/ └── Entities/
Iz nje razberete le to, da projekt uporablja neke storitve, repozitorije in entitete. O dejanskem namenu aplikacije ne izveste ničesar.
Poglejmo si drugačen pristop – organizacijo po domenah:
app/Model/ ├── Cart/ ├── Payment/ ├── Order/ └── Product/
Tukaj je drugače – na prvi pogled je jasno, da gre za spletno trgovino. Že sama imena map razkrivajo, kaj aplikacija zna – dela s plačili, naročili in izdelki.
Prvi pristop (organizacija po tipu razredov) v praksi prinaša vrsto težav: koda, ki logično sodi skupaj, je razdrobljena v različne mape in morate med njimi preskakovati. Zato bomo organizirali po domenah.
Imenski prostori
Običajno je, da struktura map 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
, bi moral
imeti imenski prostor App\Model\Product
. Ta princip pomaga pri orientaciji v kodi in poenostavlja samodejno
nalaganje.
Ednina vs množina v imenih
Opazite, da pri glavnih mapah aplikacije uporabljamo ednino: app
, config
, log
,
temp
, www
. Enako tudi znotraj aplikacije: Model
, Core
,
Presentation
. To je zato, ker vsaka od njih predstavlja en celovit koncept.
Podobno na primer app/Model/Product
predstavlja vse v zvezi z izdelki. Ne bomo ga poimenovali
Products
, ker ne gre za mapo, polno izdelkov (to bi bile tam datoteke nokia.php
,
samsung.php
). To je imenski prostor, ki vsebuje razrede za delo z izdelki – ProductRepository.php
,
ProductService.php
.
Mapa app/Tasks
je v množini zato, ker vsebuje nabor samostojnih izvedljivih skriptov –
CleanupTask.php
, ImportTask.php
. Vsak od njih je samostojna enota.
Za doslednost priporočamo uporabo:
- Ednine za imenski prostor, ki predstavlja funkcionalno celoto (čeprav dela z več entitetami)
- Množine za zbirke samostojnih enot
- V primeru negotovosti ali če o tem ne želite razmišljati, izberite ednino
Javna mapa www/
Ta mapa je edina dostopna s spleta (t.i. document-root). Pogosto se lahko srečate tudi z imenom public/
namesto
www/
– to je le vprašanje konvencije in na funkcionalnost aplikacije nima vpliva. Mapa vsebuje:
- Vstopno točko aplikacije
index.php
- Datoteko
.htaccess
s pravili za mod_rewrite (pri Apache) - Statične datoteke (CSS, JavaScript, slike)
- Naložene datoteke
Za pravilno varnost aplikacije je ključno imeti pravilno konfiguriran document-root.
Nikoli ne postavljajte v to mapo mape node_modules/
– vsebuje na tisoče datotek, ki so lahko
izvedljive in ne bi smele biti javno dostopne.
Aplikacijska mapa app/
To je glavna mapa z aplikacijsko kodo. Osnovna struktura:
app/ ├── Core/ ← infrastrukturne zadeve ├── Model/ ← poslovna logika ├── Presentation/ ← presenterji in predloge ├── Tasks/ ← ukazni skripti └── Bootstrap.php ← zagonski razred aplikacije
Bootstrap.php
je zagonski razred aplikacije, ki
inicializira okolje, nalaga konfiguracijo in ustvarja DI vsebnik.
Poglejmo si zdaj posamezne podmape podrobneje.
Presenterji in predloge
Predstavitveni del aplikacije imamo v mapi app/Presentation
. Alternativa je kratko app/UI
. To je
mesto za vse presenterje, njihove predloge in morebitne pomožne razrede.
To plast organiziramo po domenah. V kompleksnem projektu, ki združuje spletno trgovino, blog in API, bi struktura izgledala takole:
app/Presentation/ ├── Shop/ ← spletna trgovina frontend │ ├── Product/ │ ├── Cart/ │ └── Order/ ├── Blog/ ← blog │ ├── Home/ │ └── Post/ ├── Admin/ ← administracija │ ├── Dashboard/ │ └── Products/ └── Api/ ← API končne točke └── V1/
Nasprotno pa bi pri preprostem blogu uporabili členitev:
app/Presentation/ ├── Front/ ← frontend spletnega mesta │ ├── Home/ │ └── Post/ ├── Admin/ ← administracija │ ├── Dashboard/ │ └── Posts/ ├── Error/ └── Export/ ← RSS, sitemaps itd.
Mape kot Home/
ali Dashboard/
vsebujejo presenterje in predloge. Mape kot Front/
,
Admin/
ali Api/
imenujemo moduli. Tehnično gre za običajne mape, ki služijo za logično
členitev aplikacije.
Vsaka mapa s presenterjem vsebuje enako poimenovan presenter in njegove predloge. Na primer, mapa Dashboard/
vsebuje:
Dashboard/ ├── DashboardPresenter.php ← presenter └── default.latte ← predloga
Ta struktura map se odraža v imenskih prostorih razredov. Na primer, DashboardPresenter
se nahaja v imenskem
prostoru App\Presentation\Admin\Dashboard
(glej #mapiranje presenterjev):
namespace App\Presentation\Admin\Dashboard;
class DashboardPresenter extends Nette\Application\UI\Presenter
{
// ...
}
Na presenter Dashboard
znotraj modula Admin
se v aplikaciji sklicujemo s pomočjo dvopične
notacije kot na Admin:Dashboard
. Na njegovo akcijo default
potem kot na
Admin:Dashboard:default
. V primeru ugnezdenih modulov uporabljamo več dvopičij, na primer
Shop:Order:Detail:default
.
Fleksibilen razvoj strukture
Ena od velikih prednosti te strukture je, kako elegantno se prilagaja rastočim potrebam projekta. Kot primer si vzemimo del, ki generira XML vire. Na začetku imamo preprosto obliko:
Export/ ├── ExportPresenter.php ← en presenter za vse izvoze ├── sitemap.latte ← predloga za sitemap └── feed.latte ← predloga za RSS vir
Sčasoma se dodajo nove vrste virov in zanje potrebujemo več logike… Noben problem! Mapa Export/
preprosto
postane modul:
Export/ ├── Sitemap/ │ ├── SitemapPresenter.php │ └── sitemap.latte └── Feed/ ├── FeedPresenter.php ├── zbozi.latte ← vir za Zboží.cz └── heureka.latte ← vir za Heureka.cz
Ta transformacija je popolnoma gladka – dovolj je ustvariti nove podmape, razdeliti kodo vanje in posodobiti povezave (npr.
iz Export:feed
na Export:Feed:zbozi
). Zahvaljujoč temu lahko strukturo postopoma širimo glede na
potrebe, raven gnezdenja ni nikakor omejena.
Če na primer v administraciji imate veliko presenterjev, ki se nanašajo na upravljanje naročil, kot so
OrderDetail
, OrderEdit
, OrderDispatch
itd., lahko za boljšo organiziranost na tem mestu
ustvarite modul (mapo) Order
, v katerem bodo (mape za) presenterje Detail
, Edit
,
Dispatch
in drugi.
Lokacija predlog
V prejšnjih primerih smo videli, da so predloge nameščene neposredno v mapi s presenterjem:
Dashboard/ ├── DashboardPresenter.php ← presenter ├── DashboardTemplate.php ← izbirni razred za predlogo └── default.latte ← predloga
Ta lokacija se v praksi izkaže za najudobnejšo – vse povezane datoteke imate takoj pri roki.
Alternativno lahko predloge namestite v podmapo templates/
. Nette podpira obe varianti. Celo predloge lahko
namestite tudi popolnoma izven mape Presentation/
. Vse o možnostih lokacije predlog najdete v poglavju Iskanje predlog.
Pomožni razredi in komponente
K presenterjem in predlogam pogosto spadajo tudi druge pomožne datoteke. Namestimo jih logično glede na njihovo področje delovanja:
1. Neposredno pri presenterju v primeru specifičnih komponent za dani presenter:
Product/ ├── ProductPresenter.php ├── ProductGrid.php ← komponenta za izpis izdelkov └── FilterForm.php ← obrazec za filtriranje
2. Za modul – priporočamo uporabo mape Accessory
, ki se namesti pregledno takoj na začetku
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/
Ali pa lahko pomožne razrede kot LatteExtension.php
ali TemplateFilters.php
namestite
v infrastrukturno mapo app/Core/Latte/
. In komponente v app/Components
. Izbira je odvisna od
navad ekipe.
Model – srce aplikacije
Model vsebuje vso poslovno logiko aplikacije. Za njegovo organizacijo velja spet pravilo – strukturiramo po domenah:
app/Model/ ├── Payment/ ← vse v zvezi s plačili │ ├── PaymentFacade.php ← glavna vstopna točka │ ├── PaymentRepository.php │ ├── Payment.php ← entiteta ├── Order/ ← vse v zvezi z naročili │ ├── OrderFacade.php │ ├── OrderRepository.php │ ├── Order.php └── Shipping/ ← vse v zvezi z dostavo
V modelu se tipično srečate s temi tipi razredov:
Fasade: predstavljajo glavno vstopno točko v konkretno domeno v aplikaciji. Delujejo kot orkestrator, ki koordinira sodelovanje med različnimi storitvami za namen implementacije celotnih primerov uporabe (kot “ustvari naročilo” ali “obdelaj plačilo”). Pod svojo orkestracijsko plastjo fasada skriva implementacijske podrobnosti pred preostankom aplikacije, s čimer zagotavlja čist vmesnik za delo z dano domeno.
class OrderFacade
{
public function createOrder(Cart $cart): Order
{
// validacija
// ustvarjanje naročila
// pošiljanje e-pošte
// zapisovanje v statistiko
}
}
Storitve: osredotočajo se na specifično poslovno operacijo znotraj domene. Za razliko od fasade, ki orkestrira celotne primere uporabe, storitev implementira konkretno poslovno logiko (kot izračuni cen ali obdelava plačil). Storitve so tipično brez stanja in jih lahko uporabljajo bodisi fasade kot gradniki za kompleksnejše operacije ali neposredno drugi deli aplikacije za enostavnejše naloge.
class PricingService
{
public function calculateTotal(Order $order): Money
{
// izračun cene
}
}
Repozitoriji: zagotavljajo vso komunikacijo s podatkovnim skladiščem, tipično podatkovno bazo. Njegova naloga je nalaganje in shranjevanje entitet ter implementacija metod za njihovo iskanje. Repozitorij loči preostanek aplikacije od implementacijskih podrobnosti podatkovne baze 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 spreminjajo s časom. Tipično gre za razrede, preslikane na tabele podatkovne baze s pomočjo ORM (kot Nette Database Explorer ali Doctrine). Entitete lahko vsebujejo poslovna pravila, ki se nanašajo na njihove podatke, in validacijsko logiko.
// Entiteta, preslikana na tabelo podatkovne baze 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,
]);
}
}
Vrednostni objekti: nespremenljivi objekti, ki predstavljajo vrednosti brez lastne identitete – na primer denarni znesek ali e-poštni naslov. Dve instanci vrednostnega objekta z enakimi vrednostmi se štejeta za identični.
Infrastrukturna koda
Mapa Core/
(ali tudi Infrastructure/
) je dom za tehnično osnovo aplikacije. Infrastrukturna koda
tipično vključuje:
app/Core/ ├── Router/ ← usmerjanje in upravljanje URL-jev │ └── RouterFactory.php ├── Security/ ← avtentikacija in avtorizacija │ ├── Authenticator.php │ └── Authorizator.php ├── Logging/ ← dnevniško beleženje in nadzor │ ├── SentryLogger.php │ └── FileLogger.php ├── Cache/ ← plast predpomnjenja │ └── FullPageCache.php └── Integration/ ← integracija z zunanjimi storitvami ├── Slack/ └── Stripe/
Pri manjših projektih seveda zadostuje ravna členitev:
Core/ ├── RouterFactory.php ├── Authenticator.php └── QueueMailer.php
Gre za kodo, ki:
- Rešuje tehnično infrastrukturo (usmerjanje, beleženje, predpomnjenje)
- Integrira zunanje storitve (Sentry, Elasticsearch, Redis)
- Zagotavlja osnovne storitve za celotno aplikacijo (pošta, podatkovna baza)
- Je večinoma neodvisna od konkretne domene – predpomnilnik ali logger deluje enako za spletno trgovino ali blog.
Se sprašujete, ali določen razred spada sem ali v model? Ključna razlika je v tem, da koda v Core/
:
- Ne ve nič o domeni (izdelki, naročila, članki)
- Je večinoma mogoče prenesti v drug projekt
- Rešuje “kako deluje” (kako poslati pošto), ne pa “kaj dela” (kakšno pošto poslati)
Primer za boljše razumevanje:
App\Core\MailerFactory
– ustvarja instance razreda za pošiljanje e-pošte, rešuje SMTP nastavitveApp\Model\OrderMailer
– uporabljaMailerFactory
za pošiljanje e-pošte o naročilih, pozna njihove predloge in ve, kdaj se morajo poslati
Ukazni skripti
Aplikacije pogosto potrebujejo izvajanje dejavnosti izven običajnih HTTP zahtevkov – bodisi gre za obdelavo podatkov
v ozadju, vzdrževanje ali periodične naloge. Za zagon služijo preprosti skripti v mapi bin/
, samo
implementacijsko logiko pa namestimo v app/Tasks/
(po potrebi app/Commands/
).
Primer:
app/Tasks/ ├── Maintenance/ ← vzdrževalni skripti │ ├── CleanupCommand.php ← brisanje starih podatkov │ └── DbOptimizeCommand.php ← optimizacija podatkovne baze ├── Integration/ ← integracija z zunanjimi sistemi │ ├── ImportProducts.php ← uvoz iz dobaviteljskega sistema │ └── SyncOrders.php ← sinhronizacija naročil └── Scheduled/ ← redne naloge ├── NewsletterCommand.php ← pošiljanje novičnikov └── ReminderCommand.php ← obvestila strankam
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čev e-poštnih sporočil pa že spada v Tasks/
.
Naloge običajno zaženemo iz ukazne vrstice ali prek
crona. Lahko jih zaženemo tudi prek HTTP zahtevka, vendar je treba misliti na varnost. Presenter, ki nalogo zažene, je treba
zavarovati, na primer samo za prijavljene uporabnike ali z močnim žetonom in dostopom z dovoljenih IP naslovov. Pri dolgih
nalogah je treba povečati časovno omejitev skripta in uporabiti session_write_close()
, da se ne zaklene seja.
Druge možne mape
Poleg omenjenih osnovnih map lahko glede na potrebe projekta dodate druge specializirane mape. Poglejmo si najpogostejše izmed njih in njihovo uporabo:
app/ ├── Api/ ← logika za API, neodvisna od predstavitvene plasti ├── Database/ ← migracijski skripti in sejalci za testne podatke ├── Components/ ← deljene vizualne komponente po celotni aplikaciji ├── Event/ ← uporabno, če uporabljate arhitekturo, vodeno z dogodki ├── Mail/ ← e-poštne predloge in povezana logika └── Utils/ ← pomožni razredi
Za deljene vizualne komponente, uporabljene v presenterjih po celotni aplikaciji, lahko uporabite mapo
app/Components
ali app/Controls
:
app/Components/ ├── Form/ ← deljene komponente obrazcev │ ├── SignInForm.php │ └── UserForm.php ├── Grid/ ← komponente za izpise podatkov │ └── DataGrid.php └── Navigation/ ← navigacijski elementi ├── Breadcrumbs.php └── Menu.php
Sem spadajo komponente, ki imajo kompleksnejšo logiko. Če želite komponente deliti med več projekti, je priporočljivo, da jih izločite v samostojen composer paket.
V mapo app/Mail
lahko namestite upravljanje e-poštne komunikacije:
app/Mail/ ├── templates/ ← e-poštne predloge │ ├── order-confirmation.latte │ └── welcome.latte └── OrderMailer.php
Mapiranje presenterjev
Mapiranje definira pravila za izpeljavo imena razreda iz imena presenterja. Specificiramo jih v konfiguraciji pod ključem application › mapping
.
Na tej strani smo si pokazali, da presenterje nameščamo v mapo app/Presentation
(po potrebi
app/UI
). To konvencijo moramo Nette sporočiti v konfiguracijski datoteki. Dovolj je ena vrstica:
application:
mapping: App\Presentation\*\**Presenter
Kako mapiranje deluje? Za boljše razumevanje si najprej predstavljajmo aplikacijo brez modulov. Želimo, da razredi
presenterjev spadajo v imenski prostor App\Presentation
, da se presenter Home
preslika na razred
App\Presentation\HomePresenter
. Kar dosežemo s to konfiguracijo:
application:
mapping: App\Presentation\*Presenter
Mapiranje deluje tako, da ime presenterja Home
nadomesti zvezdico v maski
App\Presentation\*Presenter
, s čimer dobimo končno ime razreda App\Presentation\HomePresenter
.
Preprosto!
Kot pa vidite v primerih v tem in drugih poglavjih, razrede presenterjev nameščamo v istoimenske podmape, na primer
presenter Home
se preslika na razred App\Presentation\Home\HomePresenter
. To dosežemo z podvojitvijo
dvopičja (zahteva Nette Application 3.2):
application:
mapping: App\Presentation\**Presenter
Zdaj pristopimo k mapiranju presenterjev v module. Za vsak modul lahko definiramo specifično mapiranje:
application:
mapping:
Front: App\Presentation\Front\**Presenter
Admin: App\Presentation\Admin\**Presenter
Api: App\Api\*Presenter
Glede na to konfiguracijo se presenter Front:Home
preslika na razred
App\Presentation\Front\Home\HomePresenter
, medtem ko se presenter Api:OAuth
na razred
App\Api\OAuthPresenter
.
Ker imata modula Front
in Admin
podoben način mapiranja in takšnih modulov bo najverjetneje več,
je mogoče ustvariti splošno pravilo, ki jih nadomesti. V masko razreda tako pride nova zvezdica za modul:
application:
mapping:
*: App\Presentation\*\**Presenter
Api: App\Api\*Presenter
Deluje tudi za globlje ugnezdene strukture map, kot je na primer presenter Admin:User:Edit
, se segment z zvezdico
ponovi za vsako raven in rezultat je razred App\Presentation\Admin\User\Edit\EditPresenter
.
Alternativni zapis je namesto niza uporabiti polje, sestavljeno iz treh segmentov. Ta zapis je ekvivalenten prejšnjemu:
application:
mapping:
*: [App\Presentation, *, **Presenter]
Api: [App\Api, '', *Presenter]