Struktura katalogów aplikacji
Jak zaprojektować przejrzystą i skalowalną strukturę katalogów dla projektów w Nette Framework? Pokażemy sprawdzone praktyki, które pomogą w organizacji kodu. Dowiesz się:
- jak logicznie podzielić aplikację na katalogi
- jak zaprojektować strukturę tak, aby dobrze skalowała się wraz ze wzrostem projektu
- jakie są możliwe alternatywy i ich zalety czy wady
Ważne jest, aby wspomnieć, że sam Nette Framework nie narzuca żadnej konkretnej struktury. Jest zaprojektowany tak, aby można go było łatwo dostosować do wszelkich potrzeb i preferencji.
Podstawowa struktura projektu
Chociaż Nette Framework nie dyktuje żadnej sztywnej struktury katalogów, istnieje sprawdzony domyślny układ w postaci Web Project:
web-project/ ├── app/ ← katalog z aplikacją ├── assets/ ← pliki SCSS, JS, obrazy..., alternatywnie resources/ ├── bin/ ← skrypty dla wiersza poleceń ├── config/ ← konfiguracja ├── log/ ← zalogowane błędy ├── temp/ ← pliki tymczasowe, cache ├── tests/ ← testy ├── vendor/ ← biblioteki zainstalowane przez Composer └── www/ ← katalog publiczny (document-root)
Tę strukturę można dowolnie modyfikować zgodnie z własnymi potrzebami – zmieniać nazwy lub przenosić foldery.
Następnie wystarczy tylko zmodyfikować ścieżki względne do katalogów w pliku Bootstrap.php
i ewentualnie
composer.json
. Nic więcej nie jest potrzebne, żadna skomplikowana rekonfiguracja, żadne zmiany stałych. Nette
dysponuje inteligentną autodetekcją i automatycznie rozpozna lokalizację aplikacji, w tym jej bazę URL.
Zasady organizacji kodu
Kiedy po raz pierwszy eksplorujesz nowy projekt, powinieneś szybko się w nim zorientować. Wyobraź sobie, że rozwijasz
katalog app/Model/
i widzisz taką strukturę:
app/Model/ ├── Services/ ├── Repositories/ └── Entities/
Z niej wyczytasz tylko to, że projekt używa jakichś usług, repozytoriów i encji. O rzeczywistym celu aplikacji nie dowiesz się absolutnie nic.
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 e-sklep. Już 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 szereg problemów: kod, który jest ze sobą logicznie powiązany, jest rozproszony w różnych folderach i trzeba między nimi przeskakiwać. Dlatego będziemy organizować według domen.
Przestrzenie nazw
Jest zwyczajem, że struktura katalogów odpowiada przestrzeniom nazw w aplikacji. Oznacza to, że fizyczna lokalizacja plików
odpowiada ich namespace. Na przykład klasa umieszczona w app/Model/Product/ProductRepository.php
powinna mieć
namespace App\Model\Product
. Ta zasada pomaga w orientacji w kodzie i upraszcza autoloading.
Liczba pojedyncza vs mnoga w nazwach
Zauważ, że w głównych katalogach aplikacji używamy liczby pojedynczej: app
, config
,
log
, temp
, www
. Podobnie wewnątrz aplikacji: Model
, Core
,
Presentation
. Dzieje się tak dlatego, że każdy z nich reprezentuje jeden spójny koncept.
Podobnie np. app/Model/Product
reprezentuje wszystko związane z produktami. Nie nazwiemy tego
Products
, ponieważ nie jest to folder pełen produktów (byłyby tam pliki nokia.php
,
samsung.php
). Jest to namespace zawierający klasy do pracy z produktami – ProductRepository.php
,
ProductService.php
.
Folder app/Tasks
jest w liczbie mnogiej, ponieważ zawiera zestaw samodzielnych skryptów wykonywalnych –
CleanupTask.php
, ImportTask.php
. Każdy z nich jest samodzielną jednostką.
Dla spójności zalecamy używanie:
- Liczby pojedynczej dla namespace reprezentującego funkcjonalną całość (nawet jeśli pracuje z wieloma encjami)
- Liczby mnogiej dla kolekcji samodzielnych jednostek
- W przypadku niepewności lub jeśli nie chcesz się nad tym zastanawiać, wybierz liczbę pojedynczą
Katalog publiczny www/
Ten katalog jest jedynym dostępnym z sieci (tzw. document-root). Często można spotkać się również z nazwą
public/
zamiast www/
– jest to tylko kwestia konwencji i nie ma wpływu na funkcjonalność. Katalog
zawiera:
- Punkt wejściowy aplikacji
index.php
- Plik
.htaccess
z regułami dla mod_rewrite (w Apache) - Pliki statyczne (CSS, JavaScript, obrazy)
- Przesłane pliki
Dla prawidłowego zabezpieczenia aplikacji kluczowe jest posiadanie poprawnie skonfigurowanego 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 jest główny katalog z kodem aplikacji. Podstawowa struktura:
app/ ├── Core/ ← kwestie 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, ładuje konfigurację i tworzy kontener DI.
Przyjrzyjmy się teraz poszczególnym podkatalogom bardziej szczegółowo.
Presentery i szablony
Część prezentacyjną aplikacji mamy w katalogu app/Presentation
. Alternatywą jest krótkie
app/UI
. Jest to miejsce dla wszystkich presenterów, ich szablonów i ewentualnych klas pomocniczych.
Tę warstwę organizujemy według domen. W złożonym projekcie, który łączy e-sklep, blog i API, struktura wyglądałaby tak:
app/Presentation/ ├── Shop/ ← frontend e-sklepu │ ├── Product/ │ ├── Cart/ │ └── Order/ ├── Blog/ ← blog │ ├── Home/ │ └── Post/ ├── Admin/ ← administracja │ ├── Dashboard/ │ └── Products/ └── Api/ ← endpointy API └── V1/
Natomiast w prostym blogu użylibyśmy podziału:
app/Presentation/ ├── Front/ ← frontend strony │ ├── Home/ │ └── Post/ ├── Admin/ ← administracja │ ├── Dashboard/ │ └── Posts/ ├── Error/ └── Export/ ← RSS, mapy strony itp.
Foldery takie jak Home/
czy Dashboard/
zawierają presentery i szablony. Foldery takie jak
Front/
, Admin/
czy Api/
nazywamy modułami. Technicznie są to zwykłe katalogi,
które służą do logicznego podziału aplikacji.
Każdy folder z presenterem zawiera tak samo nazwany presenter 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
znajduje
się w przestrzeni nazw App\Presentation\Admin\Dashboard
(zobacz mapování
presenterů):
namespace App\Presentation\Admin\Dashboard;
class DashboardPresenter extends Nette\Application\UI\Presenter
{
// ...
}
Do presentera Dashboard
wewnątrz modułu Admin
odwołujemy się w aplikacji za pomocą notacji
dwukropkowej jako Admin:Dashboard
. Do jego akcji default
następnie jako
Admin:Dashboard:default
. W przypadku zagnieżdżonych modułów używamy więcej 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ą kanały XML. Na początku mamy prostą postać:
Export/ ├── ExportPresenter.php ← jeden presenter dla wszystkich eksportów ├── sitemap.latte ← szablon dla mapy strony └── feed.latte ← szablon dla kanału RSS
Z czasem pojawią się kolejne typy kanałów i będziemy potrzebować dla nich więcej logiki… Żaden problem! Folder
Export/
po prostu stanie się modułem:
Export/ ├── Sitemap/ │ ├── SitemapPresenter.php │ └── sitemap.latte └── Feed/ ├── FeedPresenter.php ├── zbozi.latte ← kanał dla Zboží.cz └── heureka.latte ← kanał dla Heureka.cz
Ta transformacja jest całkowicie płynna – wystarczy utworzyć nowe podfoldery, podzielić do nich kod i zaktualizować
linki (np. z Export:feed
na Export:Feed:zbozi
). Dzięki temu możemy strukturę stopniowo rozszerzać w
miarę potrzeb, poziom zagnieżdżenia nie jest w żaden sposób ograniczony.
Jeśli na przykład w administracji masz wiele presenterów dotyczących zarządzania zamówieniami, takich jak
OrderDetail
, OrderEdit
, OrderDispatch
itp., możesz dla lepszej organizacji w tym miejscu
utworzyć moduł (folder) Order
, w którym będą (foldery dla) presenterów Detail
, Edit
,
Dispatch
i inne.
Lokalizacja szablonów
W poprzednich przykładach widzieliśmy, że szablony są umieszczone bezpośrednio w folderze z presenterem:
Dashboard/ ├── DashboardPresenter.php ← presenter ├── DashboardTemplate.php ← opcjonalna klasa dla szablonu └── default.latte ← szablon
Ta lokalizacja w praktyce okazuje się najwygodniejsza – wszystkie powiązane pliki masz od razu pod ręką.
Alternatywnie możesz umieścić szablony w podfolderze templates/
. Nette wspiera obie warianty. Możesz nawet
umieścić szablony całkowicie poza folderem Presentation/
. Wszystko o możliwościach umieszczania szablonów
znajdziesz w rozdziale Wyszukiwanie
szablonów.
Klasy pomocnicze i komponenty
Do presenterów i szablonów często należą również inne pliki pomocnicze. Umieszczamy je logicznie według ich zakresu:
1. Bezpośrednio przy presenterze w przypadku specyficznych komponentów dla danego presentera:
Product/ ├── ProductPresenter.php ├── ProductGrid.php ← komponent do listowania produktów └── FilterForm.php ← formularz do filtrowania
2. Dla modułu – zalecamy wykorzystanie folderu Accessory
, który umieści się przejrzyście 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/
Lub możesz umieścić klasy pomocnicze takie jak LatteExtension.php
czy TemplateFilters.php
w
folderze infrastruktury app/Core/Latte/
. A komponenty w app/Components
. Wybór zależy od zwyczajów
zespołu.
Model – serce aplikacji
Model zawiera całą logikę biznesową aplikacji. Dla jego organizacji obowiązuje ponownie zasada – strukturyzujemy według domen:
app/Model/ ├── Payment/ ← wszystko związane z płatnościami │ ├── PaymentFacade.php ← główny punkt wejściowy │ ├── PaymentRepository.php │ ├── Payment.php ← encja ├── Order/ ← wszystko związane z zamówieniami │ ├── OrderFacade.php │ ├── OrderRepository.php │ ├── Order.php └── Shipping/ ← wszystko związane z wysyłką
W modelu typowo spotkasz się z tymi typami klas:
Fasady: reprezentują główny punkt wejściowy do konkretnej domeny w aplikacji. Działają jako orkiestrator, który koordynuje współpracę między różnymi usługami w celu implementacji kompletnych przypadków użycia (jak “utwórz zamówienie” lub “przetwórz płatność”). Pod swoją warstwą orkiestracji fasada ukrywa szczegóły implementacyjne przed resztą aplikacji, dostarczając czysty interfejs do pracy z daną domeną.
class OrderFacade
{
public function createOrder(Cart $cart): Order
{
// walidacja
// utworzenie zamówienia
// wysłanie e-maila
// zapisanie do statystyk
}
}
Usługi: koncentrują się na specyficznej operacji biznesowej w ramach domeny. W przeciwieństwie do fasady, która orkiestruje całe przypadki użycia, usługa implementuje konkretną logikę biznesową (jak kalkulacje cen lub przetwarzanie płatności). Usługi są typowo bezstanowe i mogą być używane albo przez fasady jako bloki budulcowe dla bardziej złożonych operacji, albo bezpośrednio przez inne części aplikacji dla prostszych zadań.
class PricingService
{
public function calculateTotal(Order $order): Money
{
// obliczenie ceny
}
}
Repozytoria: zapewniają całą komunikację z magazynem danych, typowo bazą danych. Jego zadaniem jest wczytywanie i zapisywanie encji oraz implementacja metod do ich wyszukiwania. Repozytorium odizolowuje resztę aplikacji od szczegółów implementacyjnych bazy danych i dostarcza interfejs zorientowany obiektowo do pracy z danymi.
class OrderRepository
{
public function find(int $id): ?Order
{
}
public function findByCustomer(int $customerId): array
{
}
}
Encje: obiekty reprezentujące główne koncepty biznesowe w aplikacji, które mają swoją tożsamość i zmieniają się w czasie. Typowo są to klasy mapowane na tabele bazy danych za pomocą ORM (jak Nette Database Explorer lub Doctrine). Encje mogą zawierać reguły biznesowe dotyczące ich danych oraz 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,
]);
}
}
Obiekty wartości: niemutowalne obiekty reprezentujące wartości bez własnej tożsamości – na przykład kwota pieniężna lub adres e-mail. Dwie instancje obiektu wartości z tymi samymi wartościami są uważane za identyczne.
Kod infrastruktury
Folder Core/
(lub także Infrastructure/
) jest domem dla technicznego fundamentu aplikacji. Kod
infrastruktury typowo obejmuje:
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/
W mniejszych projektach oczywiście wystarczy płaska struktura:
Core/ ├── RouterFactory.php ├── Authenticator.php └── QueueMailer.php
Chodzi o kod, który:
- Rozwiązuje problemy techniczne infrastruktury (routing, logowanie, cachowanie)
- Integruje usługi zewnętrzne (Sentry, Elasticsearch, Redis)
- Dostarcza podstawowe usługi dla całej aplikacji (mail, baza danych)
- Jest zazwyczaj niezależny od konkretnej domeny – cache lub logger działa tak samo dla e-sklepu czy bloga.
Wahasz się, czy dana klasa należy tutaj, czy do modelu? Kluczowa różnica polega na tym, że kod w Core/
:
- Nie wie nic o domenie (produkty, zamówienia, artykuły)
- Jest zazwyczaj możliwe przeniesienie go do innego projektu
- Rozwiązuje “jak to działa” (jak wysłać maila), a nie “co to robi” (jakiego maila wysłać)
Przykład dla lepszego zrozumienia:
App\Core\MailerFactory
– tworzy instancje klasy do wysyłania e-maili, obsługuje ustawienia SMTPApp\Model\OrderMailer
– używaMailerFactory
do wysyłania e-maili o zamówieniach, zna ich szablony i wie, kiedy mają być wysłane
Skrypty poleceń
Aplikacje często potrzebują wykonywać czynności poza zwykłymi żądaniami HTTP – czy to chodzi o przetwarzanie danych
w tle, konserwację, czy zadania okresowe. Do uruchamiania służą proste skrypty w katalogu bin/
, samą logikę
implementacyjną umieszczamy w app/Tasks/
(ewentualnie 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 do wysłania jednego e-maila jest częścią modelu,
masowa wysyłka tysięcy e-maili już należy do Tasks/
.
Zadania zazwyczaj uruchamiamy z wiersza poleceń lub
przez cron. Można je uruchamiać również przez żądanie HTTP, ale trzeba pamiętać o bezpieczeństwie. Presenter, który
uruchomi zadanie, trzeba zabezpieczyć, na przykład tylko dla zalogowanych użytkowników lub silnym tokenem i dostępem
z dozwolonych adresów IP. W przypadku długich zadań trzeba zwiększyć limit czasu skryptu i użyć
session_write_close()
, aby nie blokować sesji.
Inne możliwe katalogi
Oprócz wspomnianych podstawowych katalogów można w zależności od potrzeb projektu dodać inne specjalistyczne foldery. Spójrzmy na najczęstsze z nich i ich zastosowanie:
app/ ├── Api/ ← logika dla API niezależna od warstwy prezentacji ├── Database/ ← skrypty migracyjne i seedery dla danych testowych ├── Components/ ← współdzielone komponenty wizualne w całej aplikacji ├── Event/ ← przydatne jeśli używasz architektury sterowanej zdarzeniami ├── Mail/ ← szablony e-mail i powiązana logika └── Utils/ ← klasy pomocnicze
Dla współdzielonych komponentów wizualnych używanych w presenterach w całej aplikacji można użyć folderu
app/Components
lub app/Controls
:
app/Components/ ├── Form/ ← współdzielone komponenty formularzy │ ├── SignInForm.php │ └── UserForm.php ├── Grid/ ← komponenty do listowania danych │ └── DataGrid.php └── Navigation/ ← elementy nawigacyjne ├── Breadcrumbs.php └── Menu.php
Tutaj należą komponenty, które mają bardziej złożoną logikę. Jeśli chcesz współdzielić komponenty między wieloma projektami, wskazane jest wydzielenie ich do osobnego pakietu composera.
Do katalogu app/Mail
można umieścić zarządzanie komunikacją e-mail:
app/Mail/ ├── templates/ ← szablony e-mail │ ├── order-confirmation.latte │ └── welcome.latte └── OrderMailer.php
Mapowanie presenterów
Mapowanie definiuje reguły wnioskowania nazwy klasy z nazwy presentera. Specyfikujemy je w konfiguracji pod kluczem application › mapping
.
Na tej stronie pokazaliśmy, że presentery umieszczamy w folderze app/Presentation
(ewentualnie
app/UI
). Tę konwencję musimy przekazać Nette w pliku konfiguracyjnym. Wystarczy jedna linia:
application:
mapping: App\Presentation\*\**Presenter
Jak działa mapowanie? Dla lepszego zrozumienia najpierw wyobraźmy sobie aplikację bez modułów. Chcemy, aby klasy
presenterów należały do przestrzeni nazw App\Presentation
, aby presenter Home
mapował się na klasę
App\Presentation\HomePresenter
. Co osiągniemy tą konfiguracją:
application:
mapping: App\Presentation\*Presenter
Mapowanie działa tak, że nazwa presentera Home
zastępuje gwiazdkę w masce
App\Presentation\*Presenter
, przez co uzyskujemy wynikową nazwę klasy App\Presentation\HomePresenter
.
Proste!
Jak jednak widać w przykładach w tym i innych rozdziałach, klasy presenterów umieszczamy w eponimicznych podkatalogach, na
przykład presenter Home
mapuje się na klasę App\Presentation\Home\HomePresenter
. Osiągniemy to przez
podwojenie dwukropka (wymaga Nette Application 3.2):
application:
mapping: App\Presentation\**Presenter
Teraz przystąpimy do mapowania presenterów do modułów. Dla każdego modułu możemy zdefiniować specyficzne 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
, podczas gdy presenter Api:OAuth
na klasę
App\Api\OAuthPresenter
.
Ponieważ moduły Front
i Admin
mają podobny sposób mapowania i takich modułów będzie
prawdopodobnie więcej, możliwe jest utworzenie ogólnej reguły, która je zastąpi. Do maski klasy dojdzie więc 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 na przykład presenter
Admin:User:Edit
, segment z gwiazdką powtarza się dla każdego poziomu, a wynikiem jest klasa
App\Presentation\Admin\User\Edit\EditPresenter
.
Alternatywnym zapisem jest użycie zamiast stringa tablicy składającej się z trzech segmentów. Ten zapis jest ekwiwalentny z poprzednim:
application:
mapping:
*: [App\Presentation, *, **Presenter]
Api: [App\Api, '', *Presenter]