Az alkalmazás könyvtárszerkezete
Hogyan tervezzünk világos és skálázható könyvtárstruktúrát a projektek számára a Nette Frameworkben? Megmutatjuk a bevált gyakorlatokat, amelyek segítenek a kódod rendszerezésében. Megtanulhatod:
- hogyan kell logikailag strukturálni az alkalmazást könyvtárakra osztva
- hogyan tervezze meg a struktúrát úgy, hogy jól skálázható legyen a projekt növekedésével együtt
- melyek a lehetséges alternatívák és azok előnyei, illetve 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.
Alapvető projektstruktúra
Bár a Nette Framework nem diktál semmilyen rögzített könyvtárszerkezetet, van egy bevált alapértelmezett elrendezés a Web Project formájában:
web-project/ ├── app/ ← alkalmazás könyvtár ├── assets/ ← SCSS, JS fájlok, képek..., alternatívaként resources/ ├── bin/ ← parancssori szkriptek ├── config/ ← konfiguráció ├── log/ ← naplózott hibák ├── temp/ ← ideiglenes fájlok, gyorsítótár ├── tests/ ← tesztek ├── vendor/ ← a Composer által telepített könyvtárak └── www/ ← nyilvános könyvtár (document-root)
Ezt a struktúrát szabadon módosíthatja igényei szerint – átnevezhet vagy áthelyezhet mappákat. Ezután csak a
Bootstrap.php
és esetleg a composer.json
könyvtárak relatív elérési útvonalait kell
beállítania. Semmi másra nincs szükség, nincs bonyolult átkonfigurálás, nincs állandó változtatás. A Nette
intelligens automatikus felismeréssel rendelkezik, és automatikusan felismeri az alkalmazás helyét, beleértve annak
URL-bázisát is.
Kódszervezési elvek
Amikor először fedez fel egy új projektet, gyorsan tájékozódnia kell. Képzelje el, hogy rákattint a
app/Model/
könyvtárra, és ezt a struktúrát látja:
app/Model/ ├── Services/ ├── Repositories/ └── Entities/
Ebből csak annyit tudsz meg, hogy a projekt használ néhány szolgáltatást, tárolót és entitást. Az alkalmazás tényleges céljáról semmit sem fog megtudni.
Nézzünk meg egy másik megközelítést – szervezés tartományok szerint:
app/Model/ ├── Cart/ ├── Payment/ ├── Order/ └── Product/
Ez más – első pillantásra egyértelmű, hogy ez egy e-kereskedelmi oldal. Már a könyvtárnevek is elárulják, hogy mit tud az alkalmazás – fizetésekkel, megrendelésekkel és termékekkel dolgozik.
Az első megközelítés (osztályok típusa szerinti szervezés) a gyakorlatban számos problémát okoz: a logikailag összefüggő kódok különböző mappákban vannak szétszórva, és ezek között ugrálni kell. Ezért szervezünk tartományok szerint.
Névterek
Hagyományos, hogy a könyvtárszerkezet megfelel az alkalmazás névtereinek. Ez azt jelenti, hogy a fájlok fizikai helye
megfelel a névterüknek. Például egy osztály, amely a app/Model/Product/ProductRepository.php
cím alatt
található, a App\Model\Product
névtérben kell, hogy legyen. Ez az elv segíti a kódorientációt és
egyszerűsíti az automatikus betöltést.
Egyes szám vs. többes szám a nevekben
Vegyük észre, hogy a fő alkalmazási könyvtárak esetében egyes számot használunk: app
,
config
, log
, temp
, www
. Ugyanez vonatkozik az alkalmazáson belül is:
Model
, Core
, Presentation
. Ez azért van így, mert mindegyik egy egységes fogalmat
képvisel.
Hasonlóképpen, a app/Model/Product
mindent képvisel a termékekről. Nem nevezzük Products
, mert
ez nem egy termékekkel teli mappa (amely olyan fájlokat tartalmazna, mint iphone.php
, samsung.php
). Ez
egy névtér, amely a termékekkel való munkához szükséges osztályokat tartalmazza – ProductRepository.php
,
ProductService.php
.
A app/Tasks
mappa többes számban van, mert önállóan futtatható szkripteket tartalmaz –
CleanupTask.php
, ImportTask.php
. Ezek mindegyike önálló egységet alkot.
A következetesség érdekében javasoljuk a következőket használni:
- Egyes szám a funkcionális egységet képviselő névterek esetében (még akkor is, ha több egységgel dolgozunk).
- többes szám a független egységek gyűjteményeire
- Bizonytalanság esetén, vagy ha nem akarunk ezen gondolkodni, válasszuk az egyes számot.
Nyilvános címtár www/
Ez a könyvtár az egyetlen, amely a világhálóról elérhető (úgynevezett dokumentum-gyökér). Gyakran találkozhatsz a
public/
névvel a www/
helyett – ez csak konvenció kérdése, és nem befolyásolja a
funkcionalitást. A könyvtár a következőket tartalmazza:
- Az alkalmazás belépési pontja
index.php
.htaccess
fájl mod_rewrite szabályokkal (Apache esetén)- Statikus fájlok (CSS, JavaScript, képek)
- Feltöltött fájlok
A megfelelő alkalmazásbiztonság érdekében elengedhetetlen a megfelelően konfigurált document-root.
Soha ne helyezze a node_modules/
mappát ebbe a könyvtárba – ez több ezer olyan fájlt
tartalmaz, amelyek futtathatóak lehetnek, és nem lehetnek nyilvánosan hozzáférhetők.
Alkalmazási könyvtár app/
Ez a fő könyvtár az alkalmazáskóddal. Alapvető struktúra:
app/ ├── Core/ ← infrastrukturális kérdések ├── Model/ ← üzleti logika ├── Presentation/ ← prezenterek és sablonok ├── Tasks/ ← parancsszkriptek └── Bootstrap.php ← alkalmazás bootstrap osztály
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észletesen az egyes alkönyvtárakat.
Előadók és sablonok
Az alkalmazás prezentációs része a app/Presentation
könyvtárban található. Alternatív megoldás a rövid
app/UI
. Ez a hely az összes prezenter, a sablonjaik és az esetleges segédosztályok helye.
Ezt a réteget tartományok szerint szervezzük. Egy összetett projektben, amely egyesíti az e-kereskedelmet, a blogot és az API-t, a struktúra így nézne ki:
app/Presentation/ ├── Shop/ ← e-kereskedelmi 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 ezt a struktúrát használnánk:
app/Presentation/ ├── Front/ ← weboldal frontend │ ├── Home/ │ └── Post/ ├── Admin/ ← adminisztráció │ ├── Dashboard/ │ └── Posts/ ├── Error/ └── Export/ ← RSS, sitemaps stb.
A Home/
vagy a Dashboard/
mappák tartalmazzák az előadókat és a sablonokat. Az olyan mappák,
mint a Front/
, Admin/
vagy Api/
a modulok. Technikailag ezek szabályos
könyvtárak, amelyek az alkalmazás logikai szervezését szolgálják.
Minden bemutatót tartalmazó mappa tartalmaz egy hasonló nevű bemutatót és annak sablonjait. Például a
Dashboard/
mappa a következőket tartalmazza:
Dashboard/ ├── DashboardPresenter.php ← előadó └── default.latte ← sablon
Ez a könyvtárszerkezet tükröződik az osztályok névterében. Például a DashboardPresenter
a
App\Presentation\Admin\Dashboard
névtérben található (lásd az előadói
leképezést):
namespace App\Presentation\Admin\Dashboard;
class DashboardPresenter extends Nette\Application\UI\Presenter
{
//...
}
A Dashboard
prezenterre a Admin
modulon belül az alkalmazásban a Admin:Dashboard
kettőspont jelöléssel -ként hivatkozunk. A default
akciójára ezután Admin:Dashboard:default
.
Beágyazott modulok esetén több kettőspontot használunk, például Shop:Order:Detail:default
.
Rugalmas struktúrafejlesztés
Ennek a struktúrának az egyik nagy előnye, hogy milyen elegánsan alkalmazkodik a növekvő projektigényekhez. Példaként vegyük az XML feedeket generáló részt. Kezdetben van egy egyszerű űrlapunk:
Export/ ├── ExportPresenter.php ← egy előadó minden exportra ├── sitemap.latte ← sablon az oldaltérképhez └── feed.latte ← sablon RSS feedhez
Idővel több feed-típus kerül hozzá, és több logikára van szükségünk… Nem probléma! A Export/
mappából egyszerűen modul lesz:
Export/ ├── Sitemap/ │ ├── SitemapPresenter.php │ └── sitemap.latte └── Feed/ ├── FeedPresenter.php ├── amazon.latte ← takarmány az Amazon számára └── ebay.latte ← takarmány az eBay számára
Ez az átalakítás teljesen zökkenőmentes – csak hozzon létre új almappákat, ossza fel a kódot ezekre, és frissítse
a hivatkozásokat (pl. Export:feed
-ról Export:Feed:amazon
-re ). Ennek köszönhetően a struktúrát
fokozatosan, igény szerint bővíthetjük, a beágyazási szintnek semmilyen korlátja nincs.
Például, ha az adminisztrációban sok, a rendeléskezeléssel kapcsolatos bemutató van, mint például
OrderDetail
, OrderEdit
, OrderDispatch
stb, akkor a jobb szervezés érdekében
létrehozhatunk egy Order
modult (mappát), amely tartalmazni fogja a Detail
, Edit
,
Dispatch
és egyéb bemutatók (mappáit).
Sablon helye
Az előző példákban láttuk, hogy a sablonok közvetlenül az előadó mappájában találhatók:
Dashboard/ ├── DashboardPresenter.php ← előadó ├── DashboardTemplate.php ← opcionális sablon osztály └── default.latte ← sablon
A gyakorlatban ez a hely bizonyul a legkényelmesebbnek – minden kapcsolódó fájl kéznél van.
Alternatívaként a sablonokat a templates/
almappában is elhelyezheti. A Nette mindkét változatot támogatja.
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 prezenterek és sablonok gyakran más segédfájlokat is tartalmaznak. Ezeket logikusan a hatókörüknek megfelelően helyezzük el:
1. ** Közvetlenül a prezenterhez** az adott prezenterhez tartozó speciális komponensek esetén:
Product/ ├── ProductPresenter.php ├── ProductGrid.php ← komponens a terméklistához └── FilterForm.php ← szűrő űrlap
2. Modulhoz – javasoljuk a Accessory
mappát, amely szépen az ábécé elején található:
Front/ ├── Accessory/ │ ├── NavbarControl.php ← komponensek frontendhez │ └── TemplateFilters.php ├── Product/ └── Cart/
3. A teljes alkalmazáshoz – a Presentation/Accessory/
:
app/Presentation/ ├── Accessory/ │ ├── LatteExtension.php │ └── TemplateFilters.php ├── Front/ └── Admin/
Vagy elhelyezhet segédosztályokat, mint például LatteExtension.php
vagy TemplateFilters.php
az
infrastruktúra mappában app/Core/Latte/
. A komponenseket pedig a app/Components
. A választás a
csapat konvencióitól függ.
Modell – az alkalmazás szíve
A modell tartalmazza az alkalmazás teljes üzleti logikáját. Szervezésére ugyanaz a szabály érvényes – tartományok szerint strukturáljuk:
app/Model/ ├── Payment/ ← minden a fizetésekről │ ├── PaymentFacade.php ← fő belépési pont │ ├── PaymentRepository.php │ ├── Payment.php ← entitás ├── Order/ ← minden a megrendelésekről │ ├── OrderFacade.php │ ├── OrderRepository.php │ ├── Order.php └── Shipping/ ← minden a szállításról
A modellben jellemzően ilyen típusú osztályokkal találkozunk:
Arcok: az alkalmazás egy adott tartományának fő belépési pontját jelentik. Olyan 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 használati esetek (mint például a “megrendelés létrehozása” vagy a “fizetés feldolgozása”) megvalósítása érdekében. Az orkesztrációs rétegük alatt a homlokzat elrejti a megvalósítás részleteit az alkalmazás többi része elől, így egy tiszta felületet biztosít az adott területtel való munkához.
class OrderFacade
{
public function createOrder(Cart $cart): Order
{
// érvényesítés
// rendelés létrehozása
// e-mail küldés
// statisztikák írása
}
}
Szolgáltatások: egy tartományon belüli konkrét üzleti műveletekre összpontosítanak. Ellentétben a teljes felhasználási eseteket hangszerelő homlokzatokkal, egy szolgáltatás konkrét üzleti logikát valósít meg (például árkalkulációkat vagy fizetési folyamatokat). A szolgáltatások jellemzően állapot nélküliek, és vagy a homlokzatok használhatják őket építőelemként a bonyolultabb műveletekhez, vagy közvetlenül az alkalmazás más részei az egyszerűbb feladatokhoz.
class PricingService
{
public function calculateTotal(Order $order): Money
{
// árkalkuláció
}
}
Repozitóriumok: kezelik az adattárolóval, jellemzően egy adatbázissal folytatott kommunikációt. Feladatuk az entitások betöltése és mentése, valamint a keresésükhöz szükséges módszerek implementálása. A tároló megvédi az alkalmazás többi részét az adatbázis megvalósításának részleteitől, és objektumorientált felületet biztosít az adatokkal való munkához.
class OrderRepository
{
public function find(int $id): ?Order
{
}
public function findByCustomer(int $customerId): array
{
}
}
Entitások: az alkalmazás fő üzleti fogalmait reprezentáló objektumok, amelyeknek megvan az identitásuk és idővel változnak. Ezek jellemzően ORM (mint például a Nette Database Explorer vagy a Doctrine) segítségével adatbázis táblákra leképezett osztályok. Az entitások tartalmazhatnak az adataikra és az érvényesítési logikára vonatkozó üzleti szabályokat.
// Az adatbázisban szereplő táblára leképezett entitás rendelések
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 saját identitás nélküli értékeket képviselnek – például egy pénzösszeget vagy egy e-mail címet. Egy értékobjektum két példánya azonos értékekkel azonosnak tekinthető.
Infrastruktúra kód
A Core/
mappa (vagy más néven Infrastructure/
) ad otthont az alkalmazás technikai alapjának. Az
infrastrukturális kód jellemzően a következőket tartalmazza:
app/Core/ ├── Router/ ← útválasztás és URL-kezelés │ └── RouterFactory.php ├── Security/ ← hitelesítés és engedélyezés │ ├── Authenticator.php │ └── Authorizator.php ├── Logging/ ← naplózás és felügyelet │ ├── SentryLogger.php │ └── FileLogger.php ├── Cache/ ← gyorsítótárazási réteg │ └── FullPageCache.php └── Integration/ ← integráció külső szolgáltatásokkal ├── Slack/ └── Stripe/
Kisebb projektek esetében természetesen elegendő a lapos struktúra:
Core/ ├── RouterFactory.php ├── Authenticator.php └── QueueMailer.php
Ez a kód:
- Kezeli a technikai infrastruktúrát (útválasztás, naplózás, gyorsítótár).
- Külső szolgáltatásokat integrál (Sentry, Elasticsearch, Redis).
- Alapvető szolgáltatásokat nyújt az egész alkalmazás számára (levelezés, adatbázis).
- Többnyire független az adott tartománytól – a cache vagy a logger ugyanúgy működik az e-kereskedelemben vagy a blogban.
Kíváncsi, hogy egy bizonyos osztály ide vagy a modellbe tartozik-e? A legfontosabb különbség az, hogy a
Core/
:
- Semmit sem tud a domainről (termékek, rendelések, cikkek).
- Általában átvihető egy másik projektbe
- Azt oldja meg, hogy “hogyan működik” (hogyan kell levelet küldeni), nem pedig azt, hogy “mit csinál” (milyen levelet kell küldeni).
Példa a jobb megértéshez:
App\Core\MailerFactory
– létrehozza az e-mail küldő osztály példányait, kezeli az SMTP beállításokat.App\Model\OrderMailer
– aMailerFactory
-t használja a megrendelésekről szóló e-mailek küldésére, ismeri a sablonjaikat és azt, hogy mikor kell elküldeni őket.
Parancsszkriptek
Az alkalmazásoknak gyakran kell a szokásos HTTP-kéréseken kívüli feladatokat végrehajtaniuk – legyen szó
háttéradatfeldolgozásról, karbantartásról vagy időszakos feladatokról. A bin/
könyvtárban található
egyszerű parancsfájlok a végrehajtásra szolgálnak, míg a tényleges végrehajtási logika a app/Tasks/
(vagy a
app/Commands/
) könyvtárba kerül.
Példa:
app/Tasks/ ├── Maintenance/ ← karbantartási szkriptek │ ├── CleanupCommand.php ← régi adatok törlése │ └── DbOptimizeCommand.php ← adatbázis-optimalizálás ├── Integration/ ← integráció külső rendszerekkel │ ├── ImportProducts.php ← importálás szállítói rendszerből │ └── SyncOrders.php ← rendelések szinkronizálása └── Scheduled/ ← rendszeres feladatok ├── NewsletterCommand.php ← hírlevelek küldése └── ReminderCommand.php ← vevői értesítések
Mi tartozik a modellbe és mi a parancsszkriptekbe? Például egy e-mail elküldésének logikája a modell része, több ezer
e-mail tömeges elküldése a Tasks/
.
A feladatokat általában parancssorból vagy cronon
keresztül futtatjuk. HTTP-kérésen keresztül is
futtathatók, de a biztonságot figyelembe kell venni. A feladatot futtató bemutatót biztosítani kell, például csak a
bejelentkezett felhasználók számára, vagy erős tokennel és engedélyezett IP-címekről való hozzáféréssel. Hosszú
feladatok esetén meg kell növelni a szkript időkorlátját, és a session_write_close()
címet kell használni a
munkamenet lezárásának elkerülése érdekében.
Egyéb lehetséges könyvtárak
Az említett alapkönyvtárakon kívül a projekt igényeinek megfelelően más speciális mappákat is hozzáadhat. Nézzük meg a leggyakoribbakat és azok használatát:
app/ ├── Api/ ← A megjelenítési rétegtől független API logika ├── Database/ ← migrációs szkriptek és seederek a tesztadatokhoz ├── Components/ ← megosztott vizuális komponensek az egész alkalmazásban ├── Event/ ← hasznos, ha eseményvezérelt architektúrát használ ├── Mail/ ← e-mail sablonok és kapcsolódó logika └── Utils/ ← segédosztályok
A prezenterekben az egész alkalmazásban használt, megosztott vizuális komponensekhez használhatja a
app/Components
vagy a app/Controls
mappát:
app/Components/ ├── Form/ ← megosztott űrlap komponensek │ ├── SignInForm.php │ └── UserForm.php ├── Grid/ ← komponensek adatlistákhoz │ └── DataGrid.php └── Navigation/ ← navigációs elemek ├── Breadcrumbs.php └── Menu.php
Ide tartoznak az összetettebb logikájú komponensek. Ha a komponenseket több projekt között szeretné megosztani, akkor érdemes azokat egy önálló composer csomagban elkülöníteni.
A app/Mail
könyvtárban helyezheti el az e-mail kommunikáció kezelését:
app/Mail/ ├── templates/ ← e-mail sablonok │ ├── order-confirmation.latte │ └── welcome.latte └── OrderMailer.php
Előadótérképezés
A leképezés szabályokat határoz meg az osztályok nevének a bemutató nevéből történő származtatására. Ezeket a
konfigurációban a application › mapping
kulcs
alatt adjuk meg.
Ezen az oldalon megmutattuk, hogy az előadókat a app/Presentation
mappába (vagy a app/UI
)
helyezzük el. Erről a konvencióról a konfigurációs fájlban kell tájékoztatnunk a Nette-et. Egyetlen sor elég:
application:
mapping: App\Presentation\*\**Presenter
Hogyan működik a leképezés? A jobb megértéshez először képzeljünk el egy modulok nélküli alkalmazást.
Szeretnénk, ha a prezenter osztályok a App\Presentation
névtér alá tartoznának, így a Home
prezenter a App\Presentation\HomePresenter
osztályhoz tartozik. Ezt a következő konfigurációval érjük el:
application:
mapping: App\Presentation\*Presenter
A leképezés úgy működik, hogy a App\Presentation\*Presenter
maszkban a csillagot a Home
prezenter névvel helyettesítjük, ami a App\Presentation\HomePresenter
végső osztálynevet eredményezi.
Egyszerű!
Azonban, ahogy ebben és más fejezetekben található példákban is láthatjuk, a prezenter osztályokat névadó
alkönyvtárakba helyezzük, például a Home
prezenter a App\Presentation\Home\HomePresenter
osztályra
képezi le a prezentert. Ezt a kettőspont megduplázásával érjük el (Nette Application 3.2 szükséges):
application:
mapping: App\Presentation\**Presenter
Most áttérünk a prezenterek modulokba való leképezésére. Minden egyes modulhoz sajátos leképezést határozhatunk meg:
application:
mapping:
Front: App\Presentation\Front\**Presenter
Admin: App\Presentation\Admin\**Presenter
Api: App\Api\*Presenter
E konfiguráció szerint a Front:Home
bemutató a App\Presentation\Front\Home\HomePresenter
osztályhoz, míg a Api:OAuth
bemutató a App\Api\OAuthPresenter
osztályhoz tartozik.
Mivel a Front
és a Admin
modulok hasonló leképezési módszerrel rendelkeznek, és valószínűleg
több ilyen modul is lesz, létrehozható egy általános szabály, amely helyettesíti őket. Az osztálymaszkhoz egy új
csillagot adunk a modulhoz:
application:
mapping:
*: App\Presentation\*\**Presenter
Api: App\Api\*Presenter
Ez működik a mélyebben egymásba ágyazott könyvtárstruktúrák esetében is, mint például a
Admin:User:Edit
bemutató, ahol a csillaggal ellátott szegmens minden szinten megismétlődik, és a
App\Presentation\Admin\User\Edit\EditPresenter
osztályt eredményezi.
Egy alternatív jelölés az, ha a karakterlánc helyett egy három szegmensből álló tömböt használunk. Ez a jelölés egyenértékű az előzővel:
application:
mapping:
*: [App\Presentation, *, **Presenter]
Api: [App\Api, '', *Presenter]