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
.htaccess
s 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áMailerFactory
k 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]