Adresářová struktura aplikace
Jak navrhnout přehlednou a škálovatelnou adresářovou strukturu pro projekty v Nette Framework? Ukážeme si osvědčené postupy, které vám pomohou s organizací kódu. Dozvíte se:
- jak logicky rozčlenit aplikaci do adresářů
- jak strukturu navrhnout tak, aby dobře škálovala s růstem projektu
- jaké jsou možné alternativy a jejich výhody či nevýhody
Důležité je zmínit, že Nette Framework samotný na žádné konkrétní struktuře nelpí. Je navržen tak, aby se dal snadno přizpůsobit jakýmkoliv potřebám a preferencím.
Základní struktura projektu
Přestože Nette Framework nediktuje žádnou pevnou adresářovou strukturu, existuje osvědčené výchozí uspořádání v podobě Web Project:
web-project/ ├── app/ ← adresář s aplikací ├── assets/ ← soubory SCSS, JS, obrázky..., alternativně resources/ ├── bin/ ← skripty pro příkazovou řádku ├── config/ ← konfigurace ├── log/ ← logované chyby ├── temp/ ← dočasné soubory, cache ├── tests/ ← testy ├── vendor/ ← knihovny instalované Composerem └── www/ ← veřejný adresář (document-root)
Tuto strukturu můžete libovolně upravovat podle svých potřeb – složky přejmenovat či přesouvat. Poté stačí pouze
upravit relativní cesty k adresářům v souboru Bootstrap.php a případně composer.json. Nic víc
není potřeba, žádná složitá rekonfigurace, žádné změny konstant. Nette disponuje chytrou autodetekcí a automaticky
rozpozná umístění aplikace včetně její URL základny.
Principy organizace kódu
Když poprví prozkoumáváte nový projekt, měli byste se v něm rychle zorientovat. Představte si, že rozkliknete
adresář app/Model/ a uvidíte tuto strukturu:
app/Model/ ├── Services/ ├── Repositories/ └── Entities/
Z ní vyčtete jen to, že projekt používá nějaké služby, repozitáře a entity. O skutečném účelu aplikace se nedozvíte vůbec nic.
Podívejme se na jiný přístup – organizaci podle domén:
app/Model/ ├── Cart/ ├── Payment/ ├── Order/ └── Product/
Tady je to jiné – na první pohled je jasné, že jde o e-shop. Už samotné názvy adresářů prozrazují, co aplikace umí – pracuje s platbami, objednávkami a produkty.
První přístup (organizace podle typu tříd) přináší v praxi řadu problémů: kód, který spolu logicky souvisí, je roztříštěný do různých složek a musíte mezi nimi přeskakovat. Proto budeme organizovat podle domén.
Jmenné prostory
Je zvykem, že adresářová struktura koresponduje se jmennými prostory v aplikaci. To znamená, že fyzické umístění
souborů odpovídá jejich namespace. Například třída umístěná v app/Model/Product/ProductRepository.php by
měla mít namespace App\Model\Product. Tento princip pomáhá v orientaci v kódu a zjednodušuje autoloading.
Jednotné vs množné číslo v názvech
Všimněte si, že u hlavních adresářů aplikace používáme jednotné číslo: app, config,
log, temp, www. Stejně tak i uvnitř aplikace: Model, Core,
Presentation. Je to proto, že každý z nich představuje jeden ucelený koncept.
Podobně třeba app/Model/Product reprezentuje vše kolem produktů. Nenazveme to Products, protože
nejde o složku plnou produktů (to by tam byly soubory nokia.php, samsung.php). Je to namespace
obsahující třídy pro práci s produkty – ProductRepository.php, ProductService.php.
Složka app/Tasks je v množném čísle proto, že obsahuje sadu samostatných spustitelných skriptů –
CleanupTask.php, ImportTask.php. Každý z nich je samostatnou jednotkou.
Pro konzistenci doporučujeme používat:
- Jednotné číslo pro namespace reprezentující funkční celek (byť pracující s více entitami)
- Množné číslo pro kolekce samostatných jednotek
- V případě nejistoty nebo pokud nad tím nechcete přemýšlet, zvolte jednotné číslo
Veřejný adresář www/
Tento adresář je jediný přístupný z webu (tzv. document-root). Často se můžete setkat i s názvem
public/ místo www/ – je to jen otázka konvence a na funkčnost rostlináře to nemá vliv.
Adresář obsahuje:
- Vstupní bod aplikace
index.php - Soubor
.htaccesss pravidly pro mod_rewrite (u Apache) - Statické soubory (CSS, JavaScript, obrázky)
- Uploadované soubory
Pro správné zabezpečení aplikace je zásadní mít správně nakonfigurovaný document-root.
Nikdy neumisťujte do tohoto adresáře složku node_modules/ – obsahuje tisíce souborů, které
mohou být spustitelné a neměly by být veřejně dostupné.
Aplikační adresář app/
Toto je hlavní adresář s aplikačním kódem. Základní struktura:
app/ ├── Core/ ← infrastrukturní záležitosti ├── Model/ ← business logika ├── Presentation/ ← presentery a šablony ├── Tasks/ ← příkazové skripty └── Bootstrap.php ← zaváděcí třída aplikace
Bootstrap.php je startovací třída aplikace,
která inicializuje prostředí, načítá konfiguraci a vytváří DI kontejner.
Pojďme se nyní podívat na jednotlivé podadresáře podrobněji.
Presentery a šablony
Prezentační část aplikace máme v adresáři app/Presentation. Alternativou je krátké app/UI.
Je to místo pro všechny presentery, jejich šablony a případné pomocné třídy.
Tuto vrstvu organizujeme podle domén. V komplexním projektu, který kombinuje e-shop, blog a API, by struktura vypadala takto:
app/Presentation/ ├── Shop/ ← e-shop frontend │ ├── Product/ │ ├── Cart/ │ └── Order/ ├── Blog/ ← blog │ ├── Home/ │ └── Post/ ├── Admin/ ← administrace │ ├── Dashboard/ │ └── Products/ └── Api/ ← API endpointy └── V1/
Naopak u jednoduchého blogu bychom použili členění:
app/Presentation/ ├── Front/ ← frontend webu │ ├── Home/ │ └── Post/ ├── Admin/ ← administrace │ ├── Dashboard/ │ └── Posts/ ├── Error/ └── Export/ ← RSS, sitemapy atd.
Složky jako Home/ nebo Dashboard/ obsahují presentery a šablony. Složky jako Front/,
Admin/ nebo Api/ nazýváme moduly. Technicky jde o běžné adresáře, které slouží
k logickému členění aplikace.
Každá složka s presenterem obsahuje stejně pojmenovaný presenter a jeho šablony. Například složka
Dashboard/ obsahuje:
Dashboard/ ├── DashboardPresenter.php ← presenter └── default.latte ← šablona
Tato adresářová struktura se odráží ve jmenných prostorech tříd. Například DashboardPresenter se
nachází ve jmenném prostoru App\Presentation\Admin\Dashboard (viz mapování
presenterů):
namespace App\Presentation\Admin\Dashboard;
class DashboardPresenter extends Nette\Application\UI\Presenter
{
// ...
}
Na presenter Dashboard uvnitř modulu Admin odkazujeme v aplikaci pomocí dvojtečkové notace jako
na Admin:Dashboard. Na jeho akci default potom jako na Admin:Dashboard:default.
V případě zanořených modulů používáme více dvojteček, například Shop:Order:Detail:default.
Flexibilní vývoj struktury
Jednou z velkých výhod této struktury je, jak elegantně se přizpůsobuje rostoucím potřebám projektu. Jako příklad si vezměme část generující XML feedy. Na začátku máme jednoduchou podobu:
Export/ ├── ExportPresenter.php ← jeden presenter pro všechny exporty ├── sitemap.latte ← šablona pro sitemapu └── feed.latte ← šablona pro RSS feed
Časem přibydou další typy feedů a potřebujeme pro ně více logiky… Žádný problém! Složka Export/ se
jednoduše stane modulem:
Export/ ├── Sitemap/ │ ├── SitemapPresenter.php │ └── sitemap.latte └── Feed/ ├── FeedPresenter.php ├── zbozi.latte ← feed pro Zboží.cz └── heureka.latte ← feed pro Heureka.cz
Tato transformace je naprosto plynulá – stačí vytvořit nové podsložky, rozdělit do nich kód a aktualizovat odkazy
(např. z Export:feed na Export:Feed:zbozi). Díky tomu můžeme strukturu postupně rozšiřovat podle
potřeby, úroveň zanoření není nijak omezena.
Pokud například v administraci máte mnoho presenterů týkajících se správy objednávek, jako jsou
OrderDetail, OrderEdit, OrderDispatch atd., můžete pro lepší organizovanost v tomto
místě vytvořit modul (složku) Order, ve kterém budou (složky pro) presentery Detail,
Edit, Dispatch a další.
Umístění šablon
V předchozích ukázkách jsme viděli, že šablony jsou umístěny přímo ve složce s presenterem:
Dashboard/ ├── DashboardPresenter.php ← presenter ├── DashboardTemplate.php ← volitelná třída pro šablonu └── default.latte ← šablona
Toto umístění se v praxi ukazuje jako nejpohodlnější – všechny související soubory máte hned po ruce.
Alternativně můžete šablony umístit do podsložky templates/. Nette podporuje obě varianty. Dokonce můžete
šablony umístit i úplně mimo Presentation/ složku. Vše o možnostech umístění šablon najdete v kapitole
Hledání šablon.
Pomocné třídy a komponenty
K prezenterům a šablonám často patří i další pomocné soubory. Umístíme je logicky podle jejich působnosti:
1. Přímo u presenteru v případě specifických komponent pro daný presenter:
Product/ ├── ProductPresenter.php ├── ProductGrid.php ← komponenta pro výpis produktů └── FilterForm.php ← formulář pro filtrování
2. Pro modul – doporučujeme využít složku Accessory, která se umístí přehledně hned na
začátku abecedy:
Front/ ├── Accessory/ │ ├── NavbarControl.php ← komponenty pro frontend │ └── TemplateFilters.php ├── Product/ └── Cart/
3. Pro celou aplikaci – v Presentation/Accessory/:
app/Presentation/ ├── Accessory/ │ ├── LatteExtension.php │ └── TemplateFilters.php ├── Front/ └── Admin/
Nebo můžete pomocné třídy jako LatteExtension.php nebo TemplateFilters.php umístit do
infrastrukturní složky app/Core/Latte/. A komponenty do app/Components. Volba závisí na
zvyklostech týmu.
Model – srdce aplikace
Model obsahuje veškerou business logiku aplikace. Pro jeho organizaci platí opět pravidlo – strukturujeme podle domén:
app/Model/ ├── Payment/ ← vše kolem plateb │ ├── PaymentFacade.php ← hlavní vstupní bod │ ├── PaymentRepository.php │ ├── Payment.php ← entita ├── Order/ ← vše kolem objednávek │ ├── OrderFacade.php │ ├── OrderRepository.php │ ├── Order.php └── Shipping/ ← vše kolem dopravy
V modelu se typicky setkáte s těmito typy tříd:
Fasády: představují hlavní vstupní bod do konkrétní domény v aplikaci. Působí jako orchestrátor, který koordinuje spolupráci mezi různými službami za účelem implementace kompletních use-cases (jako „vytvoř objednávku“ nebo „zpracuj platbu“). Pod svojí orchestrační vrstvou fasáda skrývá implementační detaily před zbytkem aplikace, čímž poskytuje čisté rozhraní pro práci s danou doménou.
class OrderFacade
{
public function createOrder(Cart $cart): Order
{
// validace
// vytvoření objednávky
// odeslání e-mailu
// zapsání do statistik
}
}
Služby: zaměřují se na specifickou business operaci v rámci domény. Na rozdíl od fasády, která orchestruje celé use-cases, služba implementuje konkrétní byznys logiku (jako výpočty cen nebo zpracování plateb). Služby jsou typicky bezstavové a mohou být použity buď fasádami jako stavební bloky pro komplexnější operace, nebo přímo jinými částmi aplikace pro jednodušší úkony.
class PricingService
{
public function calculateTotal(Order $order): Money
{
// výpočet ceny
}
}
Repozitáře: zajišťují veškerou komunikaci s datovým úložištěm, typicky databází. Jeho úkolem je načítání a ukládání entit a implementace metod pro jejich vyhledávání. Repozitář odstiňuje zbytek aplikace od implementačních detailů databáze a poskytuje objektově orientované rozhraní pro práci s daty.
class OrderRepository
{
public function find(int $id): ?Order
{
}
public function findByCustomer(int $customerId): array
{
}
}
Entity: objekty reprezentující hlavní byznys koncepty v aplikaci, které mají svou identitu a mění se v čase. Typicky jde o třídy mapované na databázové tabulky pomocí ORM (jako Nette Database Explorer nebo Doctrine). Entity mohou obsahovat business pravidla týkající se jejich dat a validační logiku.
// Entita mapovaná na databázovou tabulku 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,
]);
}
}
Value objekty: neměnné objekty reprezentující hodnoty bez vlastní identity – například peněžní částka nebo e-mailová adresa. Dvě instance value objektu se stejnými hodnotami jsou považovány za identické.
Infrastrukturní kód
Složka Core/ (nebo také Infrastructure/) je domovem pro technický základ aplikace.
Infrastrukturní kód typicky zahrnuje:
app/Core/ ├── Router/ ← routování a URL management │ └── RouterFactory.php ├── Security/ ← autentizace a autorizace │ ├── Authenticator.php │ └── Authorizator.php ├── Logging/ ← logování a monitoring │ ├── SentryLogger.php │ └── FileLogger.php ├── Cache/ ← cachovací vrstva │ └── FullPageCache.php └── Integration/ ← integrace s ext. službami ├── Slack/ └── Stripe/
U menších projektů pochopitelně stačí ploché členění:
Core/ ├── RouterFactory.php ├── Authenticator.php └── QueueMailer.php
Jde o kód, který:
- Řeší technickou infrastrukturu (routování, logování, cachování)
- Integruje externí služby (Sentry, Elasticsearch, Redis)
- Poskytuje základní služby pro celou aplikaci (mail, databáze)
- Je většinou nezávislý na konkrétní doméně – cache nebo logger funguje stejně pro eshop či blog.
Tápete, jestli určitá třída patří sem, nebo do modelu? Klíčový rozdíl je v tom, že kód v Core/:
- Neví nic o doméně (produkty, objednávky, články)
- Je většinou možné ho přenést do jiného projektu
- Řeší „jak to funguje“ (jak poslat mail), nikoliv „co to dělá“ (jaký mail poslat)
Příklad pro lepší pochopení:
App\Core\MailerFactory– vytváří instance třídy pro odesílání e-mailů, řeší SMTP nastaveníApp\Model\OrderMailer– používáMailerFactoryk odesílání e-mailů o objednávkách, zná jejich šablony a ví, kdy se mají poslat
Příkazové skripty
Aplikace často potřebují vykonávat činnosti mimo běžné HTTP požadavky – ať už jde o zpracování dat v pozadí,
údržbu, nebo periodické úlohy. Pro spouštění slouží jednoduché skripty v adresáři bin/, samotnou
implementační logiku pak umisťujeme do app/Tasks/ (případně app/Commands/).
Příklad:
app/Tasks/ ├── Maintenance/ ← údržbové skripty │ ├── CleanupCommand.php ← mazání starých dat │ └── DbOptimizeCommand.php ← optimalizace databáze ├── Integration/ ← integrace s externími systémy │ ├── ImportProducts.php ← import z dodavatelského systému │ └── SyncOrders.php ← synchronizace objednávek └── Scheduled/ ← pravidelné úlohy ├── NewsletterCommand.php ← rozesílání newsletterů └── ReminderCommand.php ← notifikace zákazníkům
Co patří do modelu a co do příkazových skriptů? Například logika pro odeslání jednoho e-mailu je součástí modelu,
hromadná rozesílka tisíců e-mailů už patří do Tasks/.
Úlohy obvykle spouštíme z příkazového řádku nebo
přes cron. Lze je spouštět i přes HTTP požadavek, ale je nutné myslet na bezpečnost. Presenter, který úlohu spustí, je
potřeba zabezpečit, například jen pro přihlášené uživatele nebo silným tokenem a přístupem z povolených IP adres.
U dlouhých úloh je nutné zvýšit časový limit skriptu a použít session_write_close(), aby se nezamykala
session.
Další možné adresáře
Kromě zmíněných základních adresářů můžete podle potřeb projektu přidat další specializované složky. Podívejme se na nejčastější z nich a jejich použití:
app/ ├── Api/ ← logika pro API nezávislá na prezentační vrstvě ├── Database/ ← migrační skripty a seedery pro testovací data ├── Components/ ← sdílené vizuální komponenty napříč celou aplikací ├── Event/ ← užitečné pokud používáte event-driven architekturu ├── Mail/ ← e-mailové šablony a související logika └── Utils/ ← pomocné třídy
Pro sdílené vizuální komponenty používané v presenterech napříč aplikací lze použít složku
app/Components nebo app/Controls:
app/Components/ ├── Form/ ← sdílené formulářové komponenty │ ├── SignInForm.php │ └── UserForm.php ├── Grid/ ← komponenty pro výpisy dat │ └── DataGrid.php └── Navigation/ ← navigační prvky ├── Breadcrumbs.php └── Menu.php
Sem patří komponenty, které mají komplexnější logiku. Pokud chcete komponenty sdílet mezi více projekty, je vhodné je vyčlenit do samostatného composer balíčku.
Do adresáře app/Mail můžete umístit správu e-mailové komunikace:
app/Mail/ ├── templates/ ← e-mailové šablony │ ├── order-confirmation.latte │ └── welcome.latte └── OrderMailer.php
Mapování presenterů
Mapování definuje pravidla pro odvozování názvu třídy z názvu presenteru. Specifikujeme je v konfiguraci pod klíčem application › mapping.
Na této stránce jsme si ukázali, že presentery umísťujeme do složky app/Presentation (případně
app/UI). Tuto konvenci musíme Nette sdělit v konfiguračním souboru. Stačí jeden řádek:
application:
mapping: App\Presentation\*\**Presenter
Jak mapování funguje? Pro lepší pochopení si nejprve představme aplikaci bez modulů. Chceme, aby třídy presenterů
spadaly do jmenného prostoru App\Presentation, aby se presenter Home mapoval na třídu
App\Presentation\HomePresenter. Což dosáhneme touto konfigurací:
application:
mapping: App\Presentation\*Presenter
Mapování funguje tak, že název presenteru Home nahradí hvězdičku v masce
App\Presentation\*Presenter, čímž získáme výsledný název třídy App\Presentation\HomePresenter.
Jednoduché!
Jak ale vidíte v ukázkách v této a dalších kapitolách, třídy presenterů umisťujeme do eponymních podadresářů,
například presenter Home se mapuje na třídu App\Presentation\Home\HomePresenter. Toho dosáhneme
zdvojením dvojtečky (vyžaduje Nette Application 3.2):
application:
mapping: App\Presentation\**Presenter
Nyní přistoupíme k mapování presenterů do modulů. Pro každý modul můžeme definovat specifické mapování:
application:
mapping:
Front: App\Presentation\Front\**Presenter
Admin: App\Presentation\Admin\**Presenter
Api: App\Api\*Presenter
Podle této konfigurace se presenter Front:Home mapuje na třídu
App\Presentation\Front\Home\HomePresenter, zatímco presenter Api:OAuth na třídu
App\Api\OAuthPresenter.
Protože moduly Front i Admin mají podobný způsob mapování a takových modulů bude nejspíš
více, je možné vytvořit obecné pravidlo, které je nahradí. Do masky třídy tak přibude nová hvězdička pro modul:
application:
mapping:
*: App\Presentation\*\**Presenter
Api: App\Api\*Presenter
Funguje to i pro hlouběji zanořené adresářové struktury, jako je například presenter Admin:User:Edit, se
segment s hvězdičkou opakuje pro každou úroveň a výsledkem je třída
App\Presentation\Admin\User\Edit\EditPresenter.
Alternativním zápisem je místo řetězce použít pole skládající se ze tří segmentů. Tento zápis je ekvivaletní s předchozím:
application:
mapping:
*: [App\Presentation, *, **Presenter]
Api: [App\Api, '', *Presenter]