Alkalmazás könyvtárstruktúrája
Hogyan tervezzünk áttekinthető és skálázható könyvtárstruktúrát Nette Framework projektekhez? Megmutatjuk a bevált gyakorlatokat, amelyek segítenek a kód szervezésében. Megtudhatja:
- hogyan logikusan tagoljuk az alkalmazást könyvtárakba
- hogyan tervezzük meg a struktúrát úgy, hogy jól skálázódjon a projekt növekedésével
- mik a lehetséges alternatívák és azok előnyei vagy hátrányai
Fontos megemlíteni, hogy maga a Nette Framework nem ragaszkodik semmilyen konkrét struktúrához. Úgy tervezték, hogy könnyen alkalmazkodjon bármilyen igényhez és preferenciához.
A projekt alapstruktúrája
Bár a Nette Framework nem diktál semmilyen merev könyvtárstruktúrát, létezik egy bevált alapértelmezett elrendezés a Web Project formájában:
web-project/ ├── app/ ← alkalmazás könyvtára ├── assets/ ← SCSS, JS fájlok, képek..., alternatívaként resources/ ├── bin/ ← parancssori szkriptek ├── config/ ← konfiguráció ├── log/ ← logolt hibák ├── temp/ ← ideiglenes fájlok, cache ├── tests/ ← tesztek ├── vendor/ ← Composer által telepített könyvtárak └── www/ ← nyilvános könyvtár (document-root)
Ezt a struktúrát tetszés szerint módosíthatja igényei szerint – a mappákat átnevezheti vagy áthelyezheti. Ezután
csak a relatív elérési utakat kell módosítani a Bootstrap.php
fájlban és esetleg a
composer.json
-ban. Semmi másra nincs szükség, nincs bonyolult újrakonfigurálás, nincs konstansok módosítása.
A Nette okos automatikus felismeréssel rendelkezik, és automatikusan felismeri az alkalmazás helyét, beleértve annak URL
alapját is.
Kódszervezési elvek
Amikor először vizsgál meg egy új projektet, gyorsan eligazodnia kell benne. Képzelje el, hogy kibontja az
app/Model/
könyvtárat, és ezt a struktúrát látja:
app/Model/ ├── Services/ ├── Repositories/ └── Entities/
Ebből csak azt olvashatja ki, hogy a projekt valamilyen szolgáltatásokat, repository-kat és entitásokat használ. Az alkalmazás valódi céljáról semmit sem tud meg.
Nézzünk meg egy másik megközelítést – szervezés domainek szerint:
app/Model/ ├── Cart/ ├── Payment/ ├── Order/ └── Product/
Itt más a helyzet – első pillantásra világos, hogy egy webáruházról van szó. Már maguk a könyvtárnevek is elárulják, mit tud az alkalmazás – fizetésekkel, rendelésekkel és termékekkel dolgozik.
Az első megközelítés (szervezés osztálytípus szerint) a gyakorlatban számos problémát okoz: a logikailag összetartozó kód különböző mappákba van szétszórva, és ugrálnia kell közöttük. Ezért domainek szerint fogunk szervezni.
Névterek
Szokás, hogy a könyvtárstruktúra megfelel az alkalmazás névtereinek. Ez azt jelenti, hogy a fájlok fizikai
elhelyezkedése megfelel a namespace-üknek. Például az app/Model/Product/ProductRepository.php
-ban elhelyezett
osztálynak App\Model\Product
namespace-szel kellene rendelkeznie. Ez az elv segít a kódban való
tájékozódásban és egyszerűsíti az autoloadingot.
Egyes vs többes szám a nevekben
Figyelje meg, hogy az alkalmazás fő könyvtárainál egyes számot használunk: app
, config
,
log
, temp
, www
. Ugyanígy az alkalmazáson belül is: Model
,
Core
, Presentation
. Ez azért van, mert mindegyik egy-egy összefüggő koncepciót képvisel.
Hasonlóképpen például az app/Model/Product
mindent reprezentál a termékekkel kapcsolatban. Nem nevezzük
Products
-nak, mert nem egy termékekkel teli mappa (akkor nokia.php
, samsung.php
fájlok
lennének benne). Ez egy namespace, amely osztályokat tartalmaz a termékekkel való munkához –
ProductRepository.php
, ProductService.php
.
Az app/Tasks
mappa többes számban van, mert önálló futtatható szkriptek készletét tartalmazza –
CleanupTask.php
, ImportTask.php
. Mindegyik önálló egység.
A következetesség érdekében javasoljuk a következők használatát:
- Egyes szám egy funkcionális egységet reprezentáló namespace-hez (még ha több entitással is dolgozik)
- Többes szám önálló egységek gyűjteményeihez
- Bizonytalanság esetén, vagy ha nem akar ezen gondolkodni, válassza az egyes számot
Nyilvános könyvtár www/
Ez a könyvtár az egyetlen, amely a webről elérhető (ún. document-root). Gyakran találkozhat a public/
névvel is a www/
helyett – ez csak konvenció kérdése, és nincs hatással a funkcionalitásra. A könyvtár
tartalmazza:
- Az alkalmazás belépési pontját
index.php
- A
.htaccess
fájlt mod_rewrite szabályokkal (Apache esetén) - Statikus fájlokat (CSS, JavaScript, képek)
- Feltöltött fájlokat
Az alkalmazás megfelelő biztonsága érdekében elengedhetetlen a helyesen konfigurált document-root.
Soha ne helyezze ebbe a könyvtárba a node_modules/
mappát – ez több ezer fájlt tartalmaz,
amelyek futtathatók lehetnek, és nem kellene nyilvánosan elérhetőnek lenniük.
Alkalmazás könyvtára app/
Ez az alkalmazás kódjának fő könyvtára. Alapstruktúra:
app/ ├── Core/ ← infrastrukturális ügyek ├── Model/ ← üzleti logika ├── Presentation/ ← presenterek és sablonok ├── Tasks/ ← parancssori szkriptek └── Bootstrap.php ← az alkalmazás indító osztálya
A Bootstrap.php
az alkalmazás indító osztálya,
amely inicializálja a környezetet, betölti a konfigurációt és létrehozza a DI konténert.
Most nézzük meg részletesebben az egyes alkönyvtárakat.
Presenterek és sablonok
Az alkalmazás prezentációs része az app/Presentation
könyvtárban található. Alternatíva a rövid
app/UI
. Ez a hely minden presenter, azok sablonjai és esetleges segédosztályai számára.
Ezt a réteget domainek szerint szervezzük. Egy komplex projektben, amely kombinálja a webáruházat, a blogot és az API-t, a struktúra így nézne ki:
app/Presentation/ ├── Shop/ ← webáruház frontend │ ├── Product/ │ ├── Cart/ │ └── Order/ ├── Blog/ ← blog │ ├── Home/ │ └── Post/ ├── Admin/ ← adminisztráció │ ├── Dashboard/ │ └── Products/ └── Api/ ← API végpontok └── V1/
Ezzel szemben egy egyszerű blog esetében a következő tagolást használnánk:
app/Presentation/ ├── Front/ ← web frontend │ ├── Home/ │ └── Post/ ├── Admin/ ← adminisztráció │ ├── Dashboard/ │ └── Posts/ ├── Error/ └── Export/ ← RSS, sitemap-ek stb.
A Home/
vagy Dashboard/
mappák presentereket és sablonokat tartalmaznak. A Front/
,
Admin/
vagy Api/
mappákat moduloknak nevezzük. Technikailag ezek átlagos könyvtárak, amelyek
az alkalmazás logikai tagolására szolgálnak.
Minden presenter mappa tartalmaz egy azonos nevű presentert és annak sablonjait. Például a Dashboard/
mappa
tartalmazza:
Dashboard/ ├── DashboardPresenter.php ← presenter └── default.latte ← sablon
Ez a könyvtárstruktúra tükröződik az osztályok névtereiben. Például a DashboardPresenter
az
App\Presentation\Admin\Dashboard
névtérben található (lásd mapování
presenterů):
namespace App\Presentation\Admin\Dashboard;
class DashboardPresenter extends Nette\Application\UI\Presenter
{
// ...
}
Az Admin
modulon belüli Dashboard
presenterére az alkalmazásban kettőspontos jelöléssel
hivatkozunk, mint Admin:Dashboard
. Annak default
akciójára pedig mint
Admin:Dashboard:default
. Beágyazott modulok esetén több kettőspontot használunk, például
Shop:Order:Detail:default
.
A struktúra rugalmas fejlesztése
Ennek a struktúrának az egyik nagy előnye, hogy milyen elegánsan alkalmazkodik a projekt növekvő igényeihez. Vegyük példaként az XML feedeket generáló részt. Kezdetben egyszerű formában van:
Export/ ├── ExportPresenter.php ← egy presenter minden exportáláshoz ├── sitemap.latte ← sablon a sitemaphoz └── feed.latte ← sablon az RSS feedhez
Idővel újabb feed típusok jelennek meg, és több logikára van szükségünk hozzájuk… Semmi probléma! Az
Export/
mappa egyszerűen modullá válik:
Export/ ├── Sitemap/ │ ├── SitemapPresenter.php │ └── sitemap.latte └── Feed/ ├── FeedPresenter.php ├── zbozi.latte ← feed a Zboží.cz-hez └── heureka.latte ← feed a Heureka.cz-hez
Ez az átalakulás teljesen zökkenőmentes – csak új almappákat kell létrehozni, szétosztani bennük a kódot és
frissíteni a linkeket (pl. Export:feed
-ről Export:Feed:zbozi
-ra). Ennek köszönhetően a struktúrát
fokozatosan bővíthetjük igény szerint, a beágyazási szint nincs korlátozva.
Ha például az adminisztrációban sok presenter van a rendelések kezelésével kapcsolatban, mint például
OrderDetail
, OrderEdit
, OrderDispatch
stb., akkor a jobb szervezettség érdekében ezen a
ponton létrehozhat egy Order
modult (mappát), amelyben a Detail
, Edit
,
Dispatch
és további presenterek (mappái) lesznek.
Sablonok elhelyezése
Az előző példákban láttuk, hogy a sablonok közvetlenül a presenter mappájában helyezkednek el:
Dashboard/ ├── DashboardPresenter.php ← presenter ├── DashboardTemplate.php ← opcionális osztály a sablonhoz └── default.latte ← sablon
Ez az elhelyezés a gyakorlatban a legkényelmesebbnek bizonyul – minden kapcsolódó fájl kéznél van.
Alternatívaként a sablonokat elhelyezheti a templates/
almappába. A Nette mindkét változatot támogatja.
Sőt, a sablonokat akár teljesen a Presentation/
mappán kívül is elhelyezheti. A sablonok elhelyezési
lehetőségeiről mindent megtalál a Sablonok
keresése fejezetben.
Segédosztályok és komponensek
A presenterekhez és sablonokhoz gyakran tartoznak további segédfájlok is. Ezeket logikusan a hatókörük szerint helyezzük el:
1. Közvetlenül a presenter mellett, ha az adott presenterhez specifikus komponensekről van szó:
Product/ ├── ProductPresenter.php ├── ProductGrid.php ← komponens a termékek listázásához └── FilterForm.php ← űrlap a szűréshez
2. A modulhoz – javasoljuk az Accessory
mappa használatát, amely áttekinthetően az ábécé
elején helyezkedik el:
Front/ ├── Accessory/ │ ├── NavbarControl.php ← komponensek a frontendhez │ └── TemplateFilters.php ├── Product/ └── Cart/
3. Az egész alkalmazáshoz – a Presentation/Accessory/
-ban:
app/Presentation/ ├── Accessory/ │ ├── LatteExtension.php │ └── TemplateFilters.php ├── Front/ └── Admin/
Vagy elhelyezheti a segédosztályokat, mint a LatteExtension.php
vagy TemplateFilters.php
, az
infrastrukturális app/Core/Latte/
mappába. És a komponenseket az app/Components
-be. A választás a
csapat szokásaitól függ.
Model – az alkalmazás szíve
A modell tartalmazza az alkalmazás összes üzleti logikáját. Szervezésére ismét az a szabály érvényes – domainek szerint strukturálunk:
app/Model/ ├── Payment/ ← minden a fizetésekkel kapcsolatban │ ├── PaymentFacade.php ← fő belépési pont │ ├── PaymentRepository.php │ ├── Payment.php ← entitás ├── Order/ ← minden a rendelésekkel kapcsolatban │ ├── OrderFacade.php │ ├── OrderRepository.php │ ├── Order.php └── Shipping/ ← minden a szállítással kapcsolatban
A modellben tipikusan ezekkel az osztálytípusokkal találkozhat:
Fasádok (Facades): az alkalmazás egy adott domainjének fő belépési pontját képviselik. Orchestrátorként működnek, amely koordinálja a különböző szolgáltatások közötti együttműködést a teljes use-case-ek (mint a “rendelés létrehozása” vagy “fizetés feldolgozása”) implementálása érdekében. Az orchestrációs rétege alatt a fasád elrejti az implementációs részleteket az alkalmazás többi része elől, ezáltal tiszta interfészt biztosítva az adott domainnel való munkához.
class OrderFacade
{
public function createOrder(Cart $cart): Order
{
// validáció
// rendelés létrehozása
// e-mail küldése
// statisztikákba írás
}
}
Szolgáltatások (Services): egy specifikus üzleti műveletre összpontosítanak a domainen belül. Ellentétben a fasáddal, amely teljes use-case-eket orchestrál, a szolgáltatás egy konkrét üzleti logikát implementál (mint az árkalkulációk vagy a fizetések feldolgozása). A szolgáltatások tipikusan állapotmentesek, és használhatók akár fasádok által építőelemekként komplexebb műveletekhez, akár közvetlenül az alkalmazás más részei által egyszerűbb feladatokhoz.
class PricingService
{
public function calculateTotal(Order $order): Money
{
// árkalkuláció
}
}
Repository-k: biztosítják az összes kommunikációt az adattárolóval, tipikusan adatbázissal. Feladata az entitások betöltése és mentése, valamint metódusok implementálása azok kereséséhez. A repository elszigeteli az alkalmazás többi részét az adatbázis implementációs részleteitől, és objektumorientált interfészt biztosít az adatokkal való munkához.
class OrderRepository
{
public function find(int $id): ?Order
{
}
public function findByCustomer(int $customerId): array
{
}
}
Entitások: objektumok, amelyek az alkalmazás fő üzleti koncepcióit reprezentálják, saját identitással rendelkeznek és idővel változnak. Tipikusan olyan osztályokról van szó, amelyeket adatbázis táblákra map-elnek ORM segítségével (mint a Nette Database Explorer vagy a Doctrine). Az entitások tartalmazhatnak üzleti szabályokat az adataikra vonatkozóan és validációs logikát.
// Az orders adatbázis táblára map-elt entitás
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 objektumok: megváltoztathatatlan objektumok, amelyek értékeket reprezentálnak saját identitás nélkül – például pénzösszeg vagy e-mail cím. Két azonos értékű value objektum példány azonosnak tekintendő.
Infrastrukturális kód
A Core/
(vagy Infrastructure/
) mappa az alkalmazás technikai alapjának otthona. Az
infrastrukturális kód tipikusan tartalmazza:
app/Core/ ├── Router/ ← routing és URL menedzsment │ └── RouterFactory.php ├── Security/ ← authentikáció és autorizáció │ ├── Authenticator.php │ └── Authorizator.php ├── Logging/ ← logolás és monitoring │ ├── SentryLogger.php │ └── FileLogger.php ├── Cache/ ← cachovací réteg │ └── FullPageCache.php └── Integration/ ← integráció külső szolgáltatásokkal ├── Slack/ └── Stripe/
Kisebb projekteknél természetesen elegendő a lapos tagolás:
Core/ ├── RouterFactory.php ├── Authenticator.php └── QueueMailer.php
Olyan kódról van szó, amely:
- Technikai infrastruktúrát old meg (routing, logolás, cacholás)
- Külső szolgáltatásokat integrál (Sentry, Elasticsearch, Redis)
- Alapszolgáltatásokat nyújt az egész alkalmazás számára (mail, adatbázis)
- Többnyire független a konkrét domaintól – a cache vagy a logger ugyanúgy működik egy webáruház vagy egy blog esetében.
Bizonytalan, hogy egy adott osztály ide vagy a modellbe tartozik-e? A kulcsfontosságú különbség az, hogy a
Core/
-ban lévő kód:
- Nem tud semmit a domainről (termékek, rendelések, cikkek)
- Többnyire átvihető egy másik projektbe
- Azt oldja meg, “hogyan működik” (hogyan küldjön e-mailt), nem pedig azt, “mit csinál” (milyen e-mailt küldjön)
Példa a jobb megértéshez:
App\Core\MailerFactory
– létrehozza az e-mailek küldésére szolgáló osztály példányait, kezeli az SMTP beállításokatApp\Model\OrderMailer
– használja aMailerFactory
-t a rendelésekkel kapcsolatos e-mailek küldésére, ismeri azok sablonjait és tudja, mikor kell elküldeni őket
Parancssori szkriptek
Az alkalmazásoknak gyakran kell tevékenységeket végezniük a szokásos HTTP kéréseken kívül – legyen szó akár
háttérbeli adatfeldolgozásról, karbantartásról, vagy időszakos feladatokról. Futtatásukra egyszerű szkriptek szolgálnak
a bin/
könyvtárban, magát az implementációs logikát pedig az app/Tasks/
(esetleg
app/Commands/
) mappába helyezzük.
Példa:
app/Tasks/ ├── Maintenance/ ← karbantartó szkriptek │ ├── CleanupCommand.php ← régi adatok törlése │ └── DbOptimizeCommand.php ← adatbázis optimalizálása ├── Integration/ ← integráció külső rendszerekkel │ ├── ImportProducts.php ← import a beszállítói rendszerből │ └── SyncOrders.php ← rendelések szinkronizálása └── Scheduled/ ← rendszeres feladatok ├── NewsletterCommand.php ← hírlevelek kiküldése └── ReminderCommand.php ← értesítések az ügyfeleknek
Mi tartozik a modellbe és mi a parancssori szkriptekbe? Például egyetlen e-mail elküldésének logikája a modell része,
több ezer e-mail tömeges kiküldése már a Tasks/
-ba tartozik.
A feladatokat általában parancssorból vagy cron
segítségével futtatjuk. HTTP kérésen keresztül is futtathatók, de gondolni kell a biztonságra. A feladatot elindító
presentert védeni kell, például csak bejelentkezett felhasználók számára, vagy erős tokennel és hozzáféréssel
engedélyezett IP-címekről. Hosszú feladatok esetén növelni kell a szkript időkorlátját és használni kell a
session_write_close()
-t, hogy ne záródjon le a session.
További lehetséges könyvtárak
Az említett alapkönyvtárakon kívül a projekt igényei szerint további specializált mappákat is hozzáadhat. Nézzük meg a leggyakoribbakat és azok használatát:
app/ ├── Api/ ← API logika, amely független a prezentációs rétegtől ├── Database/ ← migrációs szkriptek és seederek tesztadatokhoz ├── Components/ ← megosztott vizuális komponensek az egész alkalmazásban ├── Event/ ← hasznos, ha event-driven architektúrát használ ├── Mail/ ← e-mail sablonok és kapcsolódó logika └── Utils/ ← segédosztályok
Az alkalmazásban használt megosztott vizuális komponensekhez használható az app/Components
vagy
app/Controls
mappa:
app/Components/ ├── Form/ ← megosztott űrlap komponensek │ ├── SignInForm.php │ └── UserForm.php ├── Grid/ ← komponensek adatlistázáshoz │ └── DataGrid.php └── Navigation/ ← navigációs elemek ├── Breadcrumbs.php └── Menu.php
Ide tartoznak azok a komponensek, amelyek komplexebb logikával rendelkeznek. Ha komponenseket szeretne megosztani több projekt között, célszerű őket külön composer csomagba kivonni.
Az app/Mail
könyvtárba helyezheti az e-mail kommunikáció kezelését:
app/Mail/ ├── templates/ ← e-mail sablonok │ ├── order-confirmation.latte │ └── welcome.latte └── OrderMailer.php
Presenterek map-elése
A map-elés definiálja a szabályokat az osztály nevének levezetésére a presenter nevéből. Ezeket a konfigurációban adjuk meg az application › mapping
kulcs alatt.
Ezen az oldalon megmutattuk, hogy a presentereket az app/Presentation
(esetleg app/UI
) mappába
helyezzük. Ezt a konvenciót közölnünk kell a Nette-vel a konfigurációs fájlban. Egyetlen sor elegendő:
application:
mapping: App\Presentation\*\**Presenter
Hogyan működik a map-elés? A jobb megértés érdekében először képzeljünk el egy alkalmazást modulok nélkül. Azt
szeretnénk, hogy a presenter osztályok az App\Presentation
névtérbe essenek, hogy a Home
presenter
az App\Presentation\HomePresenter
osztályra map-eljen. Ezt ezzel a konfigurációval érjük el:
application:
mapping: App\Presentation\*Presenter
A map-elés úgy működik, hogy a Home
presenter neve helyettesíti a csillagot az
App\Presentation\*Presenter
maszkban, így kapjuk meg az App\Presentation\HomePresenter
végső
osztálynevet. Egyszerű!
Ahogy azonban a példákban ebben és más fejezetekben látható, a presenter osztályokat azonos nevű alkönyvtárakba
helyezzük, például a Home
presenter az App\Presentation\Home\HomePresenter
osztályra map-el. Ezt a
kettőspont megduplázásával érjük el (Nette Application 3.2-t igényel):
application:
mapping: App\Presentation\**Presenter
Most térjünk át a presenterek modulokba való map-elésére. Minden modulhoz definiálhatunk specifikus map-elést:
application:
mapping:
Front: App\Presentation\Front\**Presenter
Admin: App\Presentation\Admin\**Presenter
Api: App\Api\*Presenter
Ezen konfiguráció szerint a Front:Home
presenter az App\Presentation\Front\Home\HomePresenter
osztályra map-el, míg az Api:OAuth
presenter az App\Api\OAuthPresenter
osztályra.
Mivel a Front
és Admin
modulok hasonló map-elési móddal rendelkeznek, és valószínűleg több
ilyen modul lesz, létrehozható egy általános szabály, amely helyettesíti őket. Az osztály maszkjába így bekerül egy új
csillag a modulhoz:
application:
mapping:
*: App\Presentation\*\**Presenter
Api: App\Api\*Presenter
Ez mélyebben beágyazott könyvtárstruktúrák esetén is működik, mint például a Admin:User:Edit
presenter,
a csillaggal jelölt szegmens minden szinten megismétlődik, és az eredmény az
App\Presentation\Admin\User\Edit\EditPresenter
osztály.
Alternatív jelölésként string helyett használhatunk egy három szegmensből álló tömböt. Ez a jelölés egyenértékű az előzővel:
application:
mapping:
*: [App\Presentation, *, **Presenter]
Api: [App\Api, '', *Presenter]