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 – a MailerFactory -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]
verzió: 4.0