Struktura katalogów aplikacji
Jak zaprojektować przejrzystą i skalowalną strukturę katalogów dla projektów w Nette Framework? Pokażemy Ci sprawdzone praktyki, które pomogą uporządkować kod. Dowiesz się:
- jak logicznie podzielić aplikację na katalogi
- jak zaprojektować strukturę, aby dobrze się skalowała wraz z rozwojem projektu
- jakie są możliwe alternatywy oraz ich zalety i wady
Warto wspomnieć, że sam Nette Framework nie wymusza żadnej konkretnej struktury. Zaprojektowano go tak, aby dawał się łatwo dostosować do dowolnych potrzeb i preferencji.
Podstawowa struktura projektu
Choć Nette Framework nie narzuca żadnej sztywnej struktury katalogów, istnieje sprawdzony układ domyślny w postaci Web Project:
web-project/ ├── app/ ← katalog aplikacji ├── assets/ ← pliki SCSS, JS, obrazy..., alternatywnie resources/ ├── bin/ ← skrypty do wiersza poleceń ├── config/ ← konfiguracja ├── log/ ← zalogowane błędy ├── temp/ ← pliki tymczasowe, cache ├── tests/ ← testy ├── vendor/ ← biblioteki zainstalowane przez Composera └── www/ ← katalog publiczny (document-root)
Strukturę tę możesz swobodnie modyfikować według swoich potrzeb: zmieniać nazwy folderów albo je przenosić. Wystarczy
wtedy dostosować ścieżki względne do katalogów w Bootstrap.php i ewentualnie composer.json. Nic
więcej nie jest potrzebne, żadnej skomplikowanej rekonfiguracji, żadnych zmian stałych. Nette ma sprytną autodetekcję
i samo rozpoznaje położenie aplikacji wraz z jej bazowym URL.
Zasady organizacji kodu
Gdy po raz pierwszy poznajesz nowy projekt, powinieneś móc szybko się w nim odnaleźć. Wyobraź sobie, że klikasz w
katalog app/Model/ i widzisz taką strukturę:
app/Model/ ├── Services/ ├── Repositories/ └── Entities/
Dowiadujesz się z tego tylko tyle, że projekt używa jakichś usług, repozytoriów i encji. O rzeczywistym przeznaczeniu aplikacji nie dowiadujesz się niczego.
Spójrzmy na inne podejście – organizację według domen:
app/Model/ ├── Cart/ ├── Payment/ ├── Order/ └── Product/
Tutaj jest inaczej – na pierwszy rzut oka widać, że chodzi o sklep internetowy. Same nazwy katalogów zdradzają, co aplikacja potrafi: pracuje z płatnościami, zamówieniami i produktami.
Pierwsze podejście (organizacja według typu klas) przynosi w praktyce kilka problemów: kod logicznie powiązany jest rozproszony po różnych folderach i trzeba między nimi skakać. Dlatego będziemy organizować według domen.
Przestrzenie nazw
Zwyczajowo struktura katalogów odpowiada przestrzeniom nazw w aplikacji. Oznacza to, że fizyczne położenie plików zgadza
się z ich przestrzenią nazw. Na przykład klasa leżąca w app/Model/Product/ProductRepository.php powinna mieć
przestrzeń nazw App\Model\Product. Zasada ta pomaga w poruszaniu się po kodzie i upraszcza autoloading.
Liczba pojedyncza kontra mnoga w nazwach
Zauważ, że dla głównych katalogów aplikacji używamy liczby pojedynczej: app, config,
log, temp, www. To samo dotyczy wnętrza aplikacji: Model, Core,
Presentation. Dzieje się tak dlatego, że każdy z nich reprezentuje jedno spójne pojęcie.
Podobnie app/Model/Product reprezentuje wszystko, co dotyczy produktów. Nie nazywamy go Products, bo
to nie jest folder pełen produktów (zawierałby pliki w rodzaju nokia.php, samsung.php). To
przestrzeń nazw zawierająca klasy do pracy z produktami: ProductRepository.php,
ProductService.php.
Folder app/Tasks jest w liczbie mnogiej, bo zawiera zestaw osobnych wykonywalnych skryptów:
CleanupTask.php, ImportTask.php. Każdy z nich jest niezależną jednostką.
Dla spójności zalecamy używać:
- liczby pojedynczej dla przestrzeni nazw reprezentujących jednostkę funkcjonalną (nawet jeśli pracuje z wieloma encjami)
- liczby mnogiej dla zbiorów niezależnych jednostek
- w razie wątpliwości albo gdy nie chcesz się nad tym zastanawiać, wybierz liczbę pojedynczą
Katalog publiczny www/
Ten katalog jako jedyny jest dostępny z sieci (document-root). Często możesz spotkać się z nazwą public/
zamiast www/ – to tylko kwestia konwencji i nie wpływa na działanie aplikacji. Katalog zawiera:
- punkt wejścia aplikacji
index.php - plik
.htaccessz regułami mod_rewrite (dla Apache) - pliki statyczne (CSS, JavaScript, obrazy)
- przesłane pliki
Dla właściwego bezpieczeństwa aplikacji kluczowe jest poprawne skonfigurowanie document-root.
Nigdy nie umieszczaj w tym katalogu folderu node_modules/ – zawiera tysiące plików, które mogą
być wykonywalne i nie powinny być publicznie dostępne.
Katalog aplikacji app/
To główny katalog zawierający kod aplikacji. Podstawowa struktura:
app/ ├── Core/ ← sprawy infrastrukturalne ├── Model/ ← logika biznesowa ├── Presentation/ ← presentery i szablony ├── Tasks/ ← skrypty poleceń └── Bootstrap.php ← klasa startowa aplikacji
Bootstrap.php to klasa startowa aplikacji, która
inicjalizuje środowisko, wczytuje konfigurację i tworzy kontener DI.
Przyjrzyjmy się teraz poszczególnym podkatalogom szczegółowo.
Presentery i szablony
Warstwa prezentacji aplikacji leży w katalogu app/Presentation. Alternatywą jest krótsze app/UI.
To miejsce na wszystkie presentery, ich szablony i ewentualne powiązane klasy pomocnicze.
Warstwę tę organizujemy według domen. W złożonym projekcie łączącym sklep internetowy, blog i API struktura wyglądałaby tak:
app/Presentation/ ├── Shop/ ← frontend sklepu │ ├── Product/ │ ├── Cart/ │ └── Order/ ├── Blog/ ← blog │ ├── Home/ │ └── Post/ ├── Admin/ ← administracja │ ├── Dashboard/ │ └── Products/ └── Api/ ← endpointy API └── V1/
Odwrotnie, dla prostego bloga użylibyśmy takiej struktury:
app/Presentation/ ├── Front/ ← frontend witryny │ ├── Home/ │ └── Post/ ├── Admin/ ← administracja │ ├── Dashboard/ │ └── Posts/ ├── Error/ └── Export/ ← RSS, mapy strony itd.
Foldery w rodzaju Home/ czy Dashboard/ zawierają presentery i szablony. Foldery w rodzaju
Front/, Admin/ czy Api/ nazywamy modułami. Technicznie to zwykłe katalogi
służące do logicznego podziału aplikacji.
Każdy folder zawierający presenter obejmuje sam plik presentera i jego szablony. Na przykład folder Dashboard/
zawiera:
Dashboard/ ├── DashboardPresenter.php ← presenter └── default.latte ← szablon
Ta struktura katalogów odzwierciedla się w przestrzeniach nazw klas. Na przykład DashboardPresenter leży w
przestrzeni nazw App\Presentation\Admin\Dashboard (zobacz Mapowanie
presenterów):
namespace App\Presentation\Admin\Dashboard;
class DashboardPresenter extends Nette\Application\UI\Presenter
{
// ...
}
Do presentera Dashboard w module Admin odwołujemy się w aplikacji zapisem z dwukropkiem jako
Admin:Dashboard. Do jego akcji default odwołujemy się potem jako Admin:Dashboard:default.
Przy zagnieżdżonych modułach używamy większej liczby dwukropków, na przykład Shop:Order:Detail:default.
Elastyczny rozwój struktury
Jedną z wielkich zalet tej struktury jest to, jak elegancko dostosowuje się do rosnących potrzeb projektu. Jako przykład weźmy część generującą feedy XML. Na początku mamy prostą postać:
Export/ ├── ExportPresenter.php ← jeden presenter dla wszystkich eksportów ├── sitemap.latte ← szablon mapy strony └── feed.latte ← szablon feedu RSS
Z czasem dochodzą kolejne typy feedów i potrzebujemy dla nich więcej logiki… Żaden problem! Folder Export/
po prostu staje się modułem:
Export/ ├── Sitemap/ │ ├── SitemapPresenter.php │ └── sitemap.latte └── Feed/ ├── FeedPresenter.php ├── amazon.latte ← feed dla Amazona └── ebay.latte ← feed dla eBaya
Ta przemiana jest całkowicie płynna – wystarczy utworzyć nowe podfoldery, podzielić między nie kod i zaktualizować
odnośniki (np. z Export:feed na Export:Feed:amazon). Dzięki temu możemy stopniowo rozbudowywać
strukturę według potrzeb, a poziom zagnieżdżenia nie jest w żaden sposób ograniczony.
Jeśli na przykład w administracji masz wiele presenterów związanych z zarządzaniem zamówieniami, takich jak
OrderDetail, OrderEdit, OrderDispatch itd., możesz dla lepszej organizacji utworzyć
moduł (folder) o nazwie Order, który będzie zawierać (foldery dla) presentery Detail,
Edit, Dispatch i inne.
Położenie szablonów
W poprzednich przykładach widzieliśmy, że szablony leżą bezpośrednio w folderze z presenterem:
Dashboard/ ├── DashboardPresenter.php ← presenter ├── DashboardTemplate.php ← opcjonalna klasa szablonu └── default.latte ← szablon
To położenie okazuje się w praktyce najwygodniejsze – masz wszystkie powiązane pliki pod ręką.
Alternatywnie możesz umieścić szablony w podfolderze templates/. Nette obsługuje oba warianty. Możesz nawet
umieścić szablony całkowicie poza folderem Presentation/. Wszystko o możliwościach położenia szablonów
znajdziesz w rozdziale Wyszukiwanie
szablonów.
Klasy pomocnicze i komponenty
Presenterom i szablonom często towarzyszą inne pliki pomocnicze. Umieszczamy je logicznie według ich zasięgu:
1. Bezpośrednio przy presenterze w przypadku komponentów specyficznych dla tego presentera:
Product/ ├── ProductPresenter.php ├── ProductGrid.php ← komponent do listowania produktów └── FilterForm.php ← formularz do filtrowania
2. Dla modułu – zalecamy folder Accessory, który wygodnie ląduje na początku alfabetu:
Front/ ├── Accessory/ │ ├── NavbarControl.php ← komponenty dla frontendu │ └── TemplateFilters.php ├── Product/ └── Cart/
3. Dla całej aplikacji – w Presentation/Accessory/:
app/Presentation/ ├── Accessory/ │ ├── LatteExtension.php │ └── TemplateFilters.php ├── Front/ └── Admin/
Alternatywnie klasy pomocnicze, takie jak LatteExtension.php czy TemplateFilters.php, możesz
umieścić w folderze infrastrukturalnym app/Core/Latte/. A komponenty w app/Components. Wybór zależy
od konwencji zespołu.
Model – serce aplikacji
Model zawiera całą logikę biznesową aplikacji. Zasada jego organizacji jest znów taka sama: strukturyzuj według domen:
app/Model/ ├── Payment/ ← wszystko o płatnościach │ ├── PaymentFacade.php ← główny punkt wejścia │ ├── PaymentRepository.php │ ├── Payment.php ← encja ├── Order/ ← wszystko o zamówieniach │ ├── OrderFacade.php │ ├── OrderRepository.php │ ├── Order.php └── Shipping/ ← wszystko o wysyłce
W modelu typowo spotkasz te rodzaje klas:
Fasady: reprezentują główny punkt wejścia do konkretnej domeny w aplikacji. Działają jak orkiestrator, koordynując współpracę różnych usług w celu zrealizowania kompletnych przypadków użycia (jak “utwórz zamówienie” czy “przetwórz płatność”). Pod swoją warstwą orkiestracji fasada ukrywa przed resztą aplikacji szczegóły implementacyjne, dostarczając tym samym czysty interfejs do pracy z daną domeną.
class OrderFacade
{
public function createOrder(Cart $cart): Order
{
// walidacja
// utworzenie zamówienia
// wysłanie e-maila
// zapis do statystyk
}
}
Usługi: skupiają się na konkretnych operacjach biznesowych w obrębie domeny. W odróżnieniu od fasad, które orkiestrują całe przypadki użycia, usługa implementuje konkretną logikę biznesową (jak obliczanie cen czy przetwarzanie płatności). Usługi są zwykle bezstanowe i mogą być używane albo przez fasady jako klocki do bardziej złożonych operacji, albo bezpośrednio przez inne części aplikacji do prostszych zadań.
class PricingService
{
public function calculateTotal(Order $order): Money
{
// obliczenie ceny
}
}
Repozytoria: zajmują się całą komunikacją z magazynem danych, zwykle z bazą danych. Ich zadaniem jest wczytywanie i zapisywanie encji oraz implementowanie metod do ich wyszukiwania. Repozytorium osłania resztę aplikacji przed szczegółami implementacyjnymi bazy danych i dostarcza obiektowy interfejs do pracy z danymi.
class OrderRepository
{
public function find(int $id): ?Order
{
}
public function findByCustomer(int $customerId): array
{
}
}
Encje: obiekty reprezentujące główne pojęcia biznesowe w aplikacji, które mają własną tożsamość i zmieniają się w czasie. Zwykle są to klasy mapowane na tabele bazy danych za pomocą ORM (jak Nette Database Explorer albo Doctrine). Encje mogą zawierać reguły biznesowe związane ze swoimi danymi i logikę walidacji.
// encja mapowana na tabelę bazy danych '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 Objects: niezmienne obiekty reprezentujące wartości bez własnej tożsamości, na przykład kwotę pieniężną albo adres e-mail. Dwie instancje value objectu o tych samych wartościach uznawane są za identyczne.
Kod infrastrukturalny
Folder Core/ (albo alternatywnie Infrastructure/) jest domem technicznego fundamentu aplikacji. Kod
infrastrukturalny obejmuje zwykle:
app/Core/ ├── Router/ ← routing i zarządzanie URL │ └── RouterFactory.php ├── Security/ ← uwierzytelnianie i autoryzacja │ ├── Authenticator.php │ └── Authorizator.php ├── Logging/ ← logowanie i monitorowanie │ ├── SentryLogger.php │ └── FileLogger.php ├── Cache/ ← warstwa cache │ └── FullPageCache.php └── Integration/ ← integracja z usługami zewnętrznymi ├── Slack/ └── Stripe/
Przy mniejszych projektach naturalnie wystarczy struktura płaska:
Core/ ├── RouterFactory.php ├── Authenticator.php └── QueueMailer.php
To kod, który:
- zajmuje się infrastrukturą techniczną (routing, logowanie, cache)
- integruje usługi zewnętrzne (Sentry, Elasticsearch, Redis)
- dostarcza podstawowe usługi dla całej aplikacji (poczta, baza danych)
- jest w większości niezależny od konkretnej domeny – cache albo logger działa tak samo dla sklepu internetowego i dla bloga
Zastanawiasz się, czy dana klasa należy tutaj, czy do modelu? Kluczowa różnica jest taka, że kod w Core/:
- nie wie nic o domenie (produkty, zamówienia, artykuły)
- zwykle da się przenieść do innego projektu
- rozwiązuje “jak to działa” (jak wysłać e-mail), a nie “co robi” (jaki e-mail wysłać)
Przykład dla lepszego zrozumienia:
App\Core\MailerFactory– tworzy instancje klasy do wysyłania e-maili, zajmuje się ustawieniami SMTPApp\Model\OrderMailer– używaMailerFactorydo wysyłania e-maili o zamówieniach, zna ich szablony i wie, kiedy mają być wysyłane
Skrypty poleceń
Aplikacje często potrzebują wykonywać czynności poza zwykłymi żądaniami HTTP, czy to przetwarzanie danych w tle, czy
konserwację, czy zadania okresowe. Do uruchamiania służą proste skrypty w katalogu bin/, a właściwa logika
implementacji umieszczana jest w app/Tasks/ (albo app/Commands/).
Przykład:
app/Tasks/ ├── Maintenance/ ← skrypty konserwacyjne │ ├── CleanupCommand.php ← usuwanie starych danych │ └── DbOptimizeCommand.php ← optymalizacja bazy danych ├── Integration/ ← integracja z systemami zewnętrznymi │ ├── ImportProducts.php ← import z systemu dostawcy │ └── SyncOrders.php ← synchronizacja zamówień └── Scheduled/ ← zadania regularne ├── NewsletterCommand.php ← wysyłanie newsletterów └── ReminderCommand.php ← powiadomienia dla klientów
Co należy do modelu, a co do skryptów poleceń? Na przykład logika wysyłania jednego e-maila jest częścią modelu,
natomiast masowe wysyłanie tysięcy e-maili należy do Tasks/.
Zadania uruchamiane są zwykle z wiersza poleceń albo przez cron: skrypt w bin/ tworzy kontener DI metodą bootConsoleApplication() i pobiera z niego
potrzebną usługę. Można je uruchamiać również przez żądanie HTTP, ale trzeba pomyśleć o bezpieczeństwie. Presenter
uruchamiający zadanie trzeba zabezpieczyć, na przykład tylko dla zalogowanych użytkowników albo silnym tokenem i dostępem
z dozwolonych adresów IP. Przy długo działających zadaniach trzeba zwiększyć limit czasu skryptu i użyć
session_write_close(), aby nie blokować sesji.
Inne możliwe katalogi
Poza wymienionymi podstawowymi katalogami możesz dodać kolejne, wyspecjalizowane foldery według potrzeb projektu. Spójrzmy na najczęstsze i ich zastosowanie:
app/ ├── Api/ ← logika API niezależna od warstwy prezentacji ├── Database/ ← skrypty migracyjne i seedery danych testowych ├── Components/ ← współdzielone komponenty wizualne w całej aplikacji ├── Event/ ← przydatne przy architekturze zdarzeniowej ├── Mail/ ← szablony e-maili i powiązana logika └── Utils/ ← klasy pomocnicze
Na współdzielone komponenty wizualne używane w presenterach w całej aplikacji możesz przeznaczyć folder
app/Components albo app/Controls:
app/Components/ ├── Form/ ← współdzielone komponenty formularzy │ ├── SignInForm.php │ └── UserForm.php ├── Grid/ ← komponenty do listowania danych │ └── DataGrid.php └── Navigation/ ← elementy nawigacji ├── Breadcrumbs.php └── Menu.php
To miejsce dla komponentów o bardziej złożonej logice. Jeśli chcesz współdzielić komponenty między wieloma projektami, warto wydzielić je do osobnego pakietu Composera.
W katalogu app/Mail możesz umieścić zarządzanie komunikacją e-mailową:
app/Mail/ ├── templates/ ← szablony e-maili │ ├── order-confirmation.latte │ └── welcome.latte └── OrderMailer.php
Mapowanie presenterów
Mapowanie definiuje reguły wyprowadzania nazwy klasy z nazwy presentera. Podajemy je w konfiguracji pod kluczem application › mapping.
Na tej stronie pokazaliśmy, że presentery umieszczamy w folderze app/Presentation (albo app/UI). Od
Nette Application 3.3 jest to konwencja domyślna, której nie trzeba konfigurować. Jeśli używasz innej struktury albo chcesz
podać mapowanie jawnie, ustawienie domyślne odpowiada temu wierszowi:
application:
mapping: App\Presentation\*\**Presenter
Jak działa mapowanie? Dla lepszego zrozumienia wyobraźmy sobie najpierw aplikację bez modułów. Chcemy, aby klasy
presenterów należały do przestrzeni nazw App\Presentation, tak aby presenter Home mapował się na
klasę App\Presentation\HomePresenter. Osiągniemy to taką konfiguracją:
application:
mapping: App\Presentation\*Presenter
Mapowanie działa tak, że gwiazdka w masce App\Presentation\*Presenter zastępowana jest nazwą presentera
Home, co daje ostateczną nazwę klasy App\Presentation\HomePresenter. Proste!
Jak jednak widzisz w przykładach w tym i innych rozdziałach, klasy presenterów umieszczamy w jednoimiennych podkatalogach,
na przykład presenter Home mapuje się na klasę App\Presentation\Home\HomePresenter. Osiągniemy to,
używając podwójnej gwiazdki ** (wymaga Nette Application 3.2.3):
application:
mapping: App\Presentation\**Presenter
Przejdźmy teraz do mapowania presenterów w modułach. Dla każdego modułu możemy zdefiniować własne mapowanie:
application:
mapping:
Front: App\Presentation\Front\**Presenter
Admin: App\Presentation\Admin\**Presenter
Api: App\Api\*Presenter
Zgodnie z tą konfiguracją presenter Front:Home mapuje się na klasę
App\Presentation\Front\Home\HomePresenter, a presenter Api:OAuth na klasę
App\Api\OAuthPresenter.
Ponieważ moduły Front i Admin mają podobny wzorzec mapowania, a takich modułów będzie zapewne
więcej, można utworzyć ogólną regułę, która je zastąpi. Do maski klasy dochodzi nowa gwiazdka dla modułu:
application:
mapping:
*: App\Presentation\*\**Presenter
Api: App\Api\*Presenter
Działa to również dla głębiej zagnieżdżonych struktur katalogów, jak presenter Admin:User:Edit, gdzie
segment z gwiazdką powtarza się dla każdego poziomu modułu, co daje klasę
App\Presentation\Admin\User\Edit\EditPresenter.
Alternatywnym zapisem jest użycie zamiast stringa tablicy złożonej z trzech segmentów. Dla pokazanych wyżej przykładów zapis ten jest równoważny poprzedniemu:
application:
mapping:
*: [App\Presentation, *, **Presenter]
Api: [App\Api, '', *Presenter]