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ą Ci zorganizować kod. Dowiesz się:
- jak logicznie zorganizować aplikację w katalogi
- jak zaprojektować strukturę, aby dobrze się skalowała wraz z rozwojem projektu
- jakie są możliwe alternatywy i ich zalety lub wady
Ważne jest, aby wspomnieć, że sam Nette Framework nie nalega na żadną konkretną strukturę. Został zaprojektowany tak, aby można go było łatwo dostosować do wszelkich potrzeb i preferencji.
Podstawowa struktura projektu
Chociaż Nette Framework nie narzuca żadnej stałej struktury katalogów, istnieje sprawdzony domyślny układ w postaci Web Project:
web-project/ ├── app/ ← katalog aplikacji ├── assets/ ← SCSS, pliki JS, obrazy..., alternatywnie resources/ ├── bin/ ← skrypty wiersza poleceń ├── config/ ← konfiguracja ├── log/ ← zarejestrowane błędy ├── temp/ ← pliki tymczasowe, pamięć podręczna ├── tests/ ← testy ├── vendor/ ← biblioteki zainstalowane przez Composer └── www/ ← katalog publiczny (document-root)
Możesz dowolnie modyfikować tę strukturę zgodnie ze swoimi potrzebami – zmieniać nazwy lub przenosić foldery.
Następnie wystarczy dostosować względne ścieżki do katalogów w Bootstrap.php
i ewentualnie
composer.json
. Nic więcej nie jest potrzebne, żadnej skomplikowanej rekonfiguracji, żadnych ciągłych zmian.
Nette posiada inteligentne automatyczne wykrywanie i automatycznie rozpoznaje lokalizację aplikacji, w tym jej bazę URL.
Zasady organizacji kodu
Kiedy po raz pierwszy odkrywasz nowy projekt, powinieneś być w stanie szybko się zorientować. Wyobraź sobie, że klikasz
na katalog app/Model/
i widzisz następującą strukturę:
app/Model/ ├── Services/ ├── Repositories/ └── Entities/
Z tego dowiesz się tylko, że projekt korzysta z niektórych usług, repozytoriów i encji. Nie dowiesz się niczego o rzeczywistym celu aplikacji.
Przyjrzyjmy się innemu podejściu – organizacji według domen:
app/Model/ ├── Cart/ ├── Payment/ ├── Order/ └── Product/
Jest inaczej – na pierwszy rzut oka widać, że jest to witryna e-commerce. Same nazwy katalogów zdradzają, co potrafi aplikacja – działa z płatnościami, zamówieniami i produktami.
Pierwsze podejście (organizacja według typu klasy) przynosi kilka problemów w praktyce: kod, który jest logicznie powiązany, jest rozproszony w różnych folderach i trzeba między nimi przeskakiwać. Dlatego będziemy organizować według domen.
Przestrzenie nazw
Konwencjonalne jest, że struktura katalogów odpowiada przestrzeniom nazw w aplikacji. Oznacza to, że fizyczna lokalizacja
plików odpowiada ich przestrzeni nazw. Na przykład, klasa znajdująca się w
app/Model/Product/ProductRepository.php
powinna mieć przestrzeń nazw App\Model\Product
. Zasada ta
pomaga w orientacji kodu i upraszcza automatyczne ładowanie.
Liczba pojedyncza a mnoga w nazwach
Zauważ, że używamy liczby pojedynczej dla głównych katalogów aplikacji: app
, config
,
log
, temp
, www
. To samo dotyczy wewnątrz aplikacji: Model
, Core
,
Presentation
. Dzieje się tak, ponieważ każdy z nich reprezentuje jedną ujednoliconą koncepcję.
Podobnie, app/Model/Product
reprezentuje wszystko o produktach. Nie nazywamy go Products
, ponieważ
nie jest to folder pełen produktów (który zawierałby pliki takie jak iphone.php
, samsung.php
). Jest
to przestrzeń nazw zawierająca klasy do pracy z produktami – ProductRepository.php
,
ProductService.php
.
Folder app/Tasks
ma liczbę mnogą, ponieważ zawiera zestaw samodzielnych skryptów wykonywalnych –
CleanupTask.php
, ImportTask.php
. Każdy z nich jest niezależną jednostką.
Dla zachowania spójności zalecamy używanie:
- Singular dla przestrzeni nazw reprezentujących jednostkę funkcjonalną (nawet w przypadku pracy z wieloma jednostkami)
- Liczba mnoga dla zbiorów niezależnych jednostek
- W przypadku niepewności lub jeśli nie chcesz o tym myśleć, wybierz liczbę pojedynczą
Katalog publiczny www/
Ten katalog jest jedynym dostępnym z sieci (tzw. document-root). Często można spotkać się z nazwą public/
zamiast www/
– jest to tylko kwestia konwencji i nie ma wpływu na funkcjonalność. Katalog zawiera:
- Punkt wejścia aplikacji
index.php
- Plik
.htaccess
z regułami mod_rewrite (dla Apache) - Pliki statyczne (CSS, JavaScript, obrazy)
- Przesłane pliki
Dla prawidłowego bezpieczeństwa aplikacji kluczowe znaczenie ma poprawnie skonfigurowany document-root.
Nigdy nie umieszczaj folderu node_modules/
w tym katalogu – zawiera on tysiące plików, które
mogą być wykonywalne i nie powinny być publicznie dostępne.
Katalog aplikacji app/
Jest to główny katalog z kodem aplikacji. Podstawowa struktura:
app/ ├── Core/ ← Infrastruktura ma znaczenie ├── Model/ ← logika biznesowa ├── Presentation/ ← prezentery i szablony ├── Tasks/ ← skrypty poleceń └── Bootstrap.php ← klasa bootstrap aplikacji
Bootstrap.php
to klasa startowa aplikacji, która
inicjalizuje środowisko, ładuje konfigurację i tworzy kontener DI.
Przyjrzyjmy się teraz szczegółowo poszczególnym podkatalogom.
Prezentery i szablony
Część prezentacyjna aplikacji znajduje się w katalogu app/Presentation
. Alternatywą jest krótki
app/UI
. Jest to miejsce dla wszystkich prezenterów, ich szablonów i wszelkich klas pomocniczych.
Organizujemy tę warstwę według domen. W złożonym projekcie, który łączy e-commerce, blog i API, struktura wyglądałaby następująco:
app/Presentation/ ├── Shop/ ← frontend e-commerce │ ├── Product/ │ ├── Cart/ │ └── Order/ ├── Blog/ ← blog │ ├── Home/ │ └── Post/ ├── Admin/ ← administracja │ ├── Dashboard/ │ └── Products/ └── Api/ ← Punkty końcowe API └── V1/
I odwrotnie, dla prostego bloga użylibyśmy tej struktury:
app/Presentation/ ├── Front/ ← frontend strony │ ├── Home/ │ └── Post/ ├── Admin/ ← administracja │ ├── Dashboard/ │ └── Posts/ ├── Error/ └── Export/ ← RSS, mapy witryn itp.
Foldery takie jak Home/
lub Dashboard/
zawierają prezentery i szablony. Foldery takie jak
Front/
, Admin/
lub Api/
nazywane są modułami. Z technicznego punktu widzenia są
to zwykłe katalogi, które służą do logicznej organizacji aplikacji.
Każdy folder z prezenterem zawiera podobnie nazwany prezenter i jego szablony. Na przykład folder Dashboard/
zawiera:
Dashboard/ ├── DashboardPresenter.php ← prezenter └── default.latte ← szablon
Ta struktura katalogów jest odzwierciedlona w przestrzeniach nazw klas. Na przykład DashboardPresenter
znajduje
się w przestrzeni nazw App\Presentation\Admin\Dashboard
(zobacz mapowanie
prezentera):
namespace App\Presentation\Admin\Dashboard;
class DashboardPresenter extends Nette\Application\UI\Presenter
{
//...
}
Odnosimy się do prezentera Dashboard
wewnątrz modułu Admin
w aplikacji używając notacji
dwukropka jako Admin:Dashboard
. Do jego akcji default
następnie jako
Admin:Dashboard:default
. W przypadku modułów zagnieżdżonych używamy więcej dwukropków, na przykład
Shop:Order:Detail:default
.
Elastyczny rozwój struktury
Jedną z największych zalet tej struktury jest to, jak elegancko dostosowuje się ona do rosnących potrzeb projektu. Jako przykład weźmy część generującą kanały XML. Początkowo mamy prosty formularz:
Export/ ├── ExportPresenter.php ← Jeden prezenter dla wszystkich eksportów ├── sitemap.latte ← szablon dla mapy strony └── feed.latte ← szablon dla kanału RSS
Z czasem dodajemy więcej typów feedów i potrzebujemy dla nich więcej logiki… Żaden problem! Folder
Export/
staje się po prostu modułem:
Export/ ├── Sitemap/ │ ├── SitemapPresenter.php │ └── sitemap.latte └── Feed/ ├── FeedPresenter.php ├── amazon.latte ← kanał dla Amazon └── ebay.latte ← kanał dla eBay
Transformacja ta jest całkowicie bezproblemowa – wystarczy utworzyć nowe podfoldery, podzielić na nie kod
i zaktualizować odnośniki (np. z Export:feed
na Export:Feed:amazon
). Dzięki temu możemy stopniowo
rozbudowywać strukturę w miarę potrzeb, poziom zagnieżdżenia nie jest w żaden sposób ograniczony.
Przykładowo, jeśli w administracji mamy wiele prezenterów związanych z zarządzaniem zamówieniami, takich jak
OrderDetail
, OrderEdit
, OrderDispatch
itp. to dla lepszej organizacji możemy utworzyć
moduł (folder) Order
, który będzie zawierał (foldery dla) prezenterów Detail
, Edit
,
Dispatch
i innych.
Lokalizacja szablonu
W poprzednich przykładach widzieliśmy, że szablony znajdują się bezpośrednio w folderze z prezenterem:
Dashboard/ ├── DashboardPresenter.php ← prezenter ├── DashboardTemplate.php ← opcjonalna klasa szablonu └── default.latte ← szablon
Ta lokalizacja okazuje się najwygodniejsza w praktyce – masz wszystkie powiązane pliki pod ręką.
Alternatywnie można umieścić szablony w podfolderze templates/
. Nette obsługuje oba warianty. Można nawet
umieścić szablony całkowicie poza folderem Presentation/
. Wszystko na temat opcji lokalizacji szablonów można
znaleźć w rozdziale Template Lookup.
Klasy pomocnicze i komponenty
Prezentery i szablony często są dostarczane z innymi plikami pomocniczymi. Umieszczamy je logicznie zgodnie z ich zakresem:
1. Bezpośrednio z prezenterem w przypadku specyficznych komponentów dla danego prezentera:
Product/ ├── ProductPresenter.php ├── ProductGrid.php ← komponent dla listy produktów └── FilterForm.php ← formularz do filtrowania
2. Dla modułu – zalecamy korzystanie z folderu Accessory
, który jest umieszczony na początku
alfabetu:
Front/ ├── Accessory/ │ ├── NavbarControl.php ← komponenty dla frontendu │ └── TemplateFilters.php ├── Product/ └── Cart/
3. Dla całej aplikacji – w folderze Presentation/Accessory/
:
app/Presentation/ ├── Accessory/ │ ├── LatteExtension.php │ └── TemplateFilters.php ├── Front/ └── Admin/
Można też umieścić klasy pomocnicze, takie jak LatteExtension.php
lub TemplateFilters.php
w
folderze infrastruktury 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. Dla jego organizacji obowiązuje ta sama zasada – strukturyzujemy według domen:
app/Model/ ├── Payment/ ← wszystko o płatnościach │ ├── PaymentFacade.php ← główny punkt wejścia │ ├── PaymentRepository.php │ ├── Payment.php ← podmiot ├── Order/ ← wszystko o zamówieniach │ ├── OrderFacade.php │ ├── OrderRepository.php │ ├── Order.php └── Shipping/ ← wszystko o wysyłce
W modelu zazwyczaj spotyka się następujące typy klas:
Facades: reprezentują główny punkt wejścia do określonej domeny w aplikacji. Działają jako orkiestrator, który koordynuje współpracę między różnymi usługami w celu wdrożenia kompletnych przypadków użycia (takich jak “utwórz zamówienie” lub “przetwarzaj płatność”). Pod warstwą orkiestracji fasada ukrywa szczegóły implementacji przed resztą aplikacji, zapewniając w ten sposób czysty interfejs do pracy z daną domeną.
class OrderFacade
{
public function createOrder(Cart $cart): Order
{
// walidacja
// tworzenie zamówień wysyłanie wiadomości
// wysyłanie wiadomości e-mail
// zapis do statystyk
}
}
Usługi: koncentrują się na konkretnych operacjach biznesowych w domenie. W przeciwieństwie do fasad, które orkiestrują całe przypadki użycia, usługa implementuje określoną logikę biznesową (np. obliczenia cen lub przetwarzanie płatności). Usługi są zazwyczaj bezstanowe i mogą być używane przez fasady jako bloki konstrukcyjne dla bardziej złożonych operacji lub bezpośrednio przez inne części aplikacji do prostszych zadań.
class PricingService
{
public function calculateTotal(Order $order): Money
{
// kalkulacja ceny
}
}
Repozytoria: obsługują całą komunikację z magazynem danych, zazwyczaj bazą danych. Ich zadaniem jest ładowanie i zapisywanie encji oraz implementacja metod ich wyszukiwania. Repozytorium chroni resztę aplikacji przed szczegółami implementacji bazy danych i zapewnia obiektowy interfejs do pracy z danymi.
class OrderRepository
{
public function find(int $id): ?Order
{
}
public function findByCustomer(int $customerId): array
{
}
}
Entities: obiekty reprezentujące główne koncepcje biznesowe w aplikacji, które mają swoją tożsamość i zmieniają się w czasie. Zazwyczaj są to klasy mapowane do tabel bazy danych przy użyciu ORM (takich jak Nette Database Explorer lub Doctrine). Podmioty mogą zawierać reguły biznesowe dotyczące ich danych i logiki walidacji.
// Podmiot zmapowany do tabeli zamówień bazy danych
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: niezmienne obiekty reprezentujące wartości bez własnej tożsamości – na przykład kwota pieniędzy lub adres e-mail. Dwie instancje obiektu wartości z tymi samymi wartościami są uważane za identyczne.
Kod infrastruktury
Folder Core/
(lub również Infrastructure/
) jest domem dla technicznych podstaw aplikacji. Kod
infrastruktury zazwyczaj zawiera:
app/Core/ ├── Router/ ← routing i zarządzanie adresami URL │ └── RouterFactory.php ├── Security/ ← uwierzytelnianie i autoryzacja │ ├── Authenticator.php │ └── Authorizator.php ├── Logging/ ← rejestrowanie i monitorowanie │ ├── SentryLogger.php │ └── FileLogger.php ├── Cache/ ← warstwa buforowania │ └── FullPageCache.php └── Integration/ ← integracja z dodatkowymi usługami ├── Slack/ └── Stripe/
W przypadku mniejszych projektów płaska struktura jest naturalnie wystarczająca:
Core/ ├── RouterFactory.php ├── Authenticator.php └── QueueMailer.php
To jest kod, który:
- obsługuje infrastrukturę techniczną (routing, logowanie, buforowanie)
- Integruje usługi zewnętrzne (Sentry, Elasticsearch, Redis)
- Zapewnia podstawowe usługi dla całej aplikacji (poczta, baza danych)
- Jest w większości niezależny od konkretnej domeny – cache lub logger działa tak samo dla e-commerce lub bloga.
Zastanawiasz się, czy dana klasa należy do tego miejsca, czy do modelu? Kluczową różnicą jest to, że kod w
Core/
:
- Nie wie nic o domenie (produkty, zamówienia, artykuły)
- Zazwyczaj może być przeniesiony do innego projektu
- Rozwiązuje “jak to działa” (jak wysłać pocztę), a nie “co to robi” (jaką pocztę wysłać)
Przykład dla lepszego zrozumienia:
App\Core\MailerFactory
– tworzy instancje klasy wysyłającej e-maile, obsługuje ustawienia SMTPApp\Model\OrderMailer
– używaMailerFactory
do wysyłania e-maili o zamówieniach, zna ich szablony i wie, kiedy powinny zostać wysłane.
Skrypty poleceń
Aplikacje często muszą wykonywać zadania poza zwykłymi żądaniami HTTP – niezależnie od tego, czy jest to
przetwarzanie danych w tle, konserwacja czy zadania okresowe. Proste skrypty w katalogu bin/
są używane do
wykonania, podczas gdy rzeczywista logika implementacji jest umieszczona w app/Tasks/
(lub
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/ ← regularne zadania ├── 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 jednej wiadomości e-mail jest częścią
modelu, masowe wysyłanie tysięcy wiadomości e-mail należy do Tasks/
.
Zadania są zwykle uruchamiane z wiersza poleceń lub
przez cron. Mogą być również uruchamiane za pośrednictwem żądania HTTP, ale należy wziąć pod uwagę bezpieczeństwo.
Prezenter, który uruchamia zadanie, musi być zabezpieczony, na przykład tylko dla zalogowanych użytkowników lub z silnym
tokenem i dostępem z dozwolonych adresów IP. W przypadku długich zadań konieczne jest zwiększenie limitu czasu skryptu
i użycie session_write_close()
, aby uniknąć blokowania sesji.
Inne możliwe katalogi
Oprócz wspomnianych podstawowych katalogów, można dodać inne wyspecjalizowane foldery zgodnie z potrzebami projektu. Przyjrzyjmy się najpopularniejszym z nich i ich zastosowaniu:
app/ ├── Api/ ← Logika API niezależna od warstwy prezentacji ├── Database/ ← skrypty migracyjne i siewniki danych testowych ├── Components/ ← współdzielone komponenty wizualne w całej aplikacji ├── Event/ ← przydatne w przypadku korzystania z architektury sterowanej zdarzeniami ├── Mail/ ← szablony wiadomości e-mail i powiązana logika └── Utils/ ← klasy pomocnicze
W przypadku współdzielonych komponentów wizualnych używanych w prezenterach 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 dla list danych │ └── DataGrid.php └── Navigation/ ← elementy nawigacyjne ├── Breadcrumbs.php └── Menu.php
To tutaj znajdują się komponenty z bardziej złożoną logiką. Jeśli chcesz współdzielić komponenty między wieloma projektami, dobrze jest oddzielić je w samodzielnym pakiecie kompozytora.
W katalogu app/Mail
można umieścić zarządzanie komunikacją e-mail:
app/Mail/ ├── templates/ ← szablony wiadomości e-mail │ ├── order-confirmation.latte │ └── welcome.latte └── OrderMailer.php
Mapowanie prezentera
Mapowanie definiuje zasady wyprowadzania nazw klas z nazw prezenterów. Określamy je w konfiguracji pod kluczem application › mapping
.
Na tej stronie pokazaliśmy, że umieszczamy prezenterów w folderze app/Presentation
(lub app/UI
).
Musimy poinformować Nette o tej konwencji w pliku konfiguracyjnym. Wystarczy jedna linijka:
application:
mapping: App\Presentation\*\**Presenter
Jak działa mapowanie? Aby lepiej to zrozumieć, wyobraźmy sobie najpierw aplikację bez modułów. Chcemy, aby klasy
prezenterów należały do przestrzeni nazw App\Presentation
, tak aby prezenter Home
był mapowany na
klasę App\Presentation\HomePresenter
. Można to osiągnąć za pomocą tej konfiguracji:
application:
mapping: App\Presentation\*Presenter
Mapowanie działa poprzez zastąpienie gwiazdki w masce App\Presentation\*Presenter
nazwą prezentera
Home
, w wyniku czego otrzymujemy ostateczną nazwę klasy App\Presentation\HomePresenter
. Proste!
Jednakże, jak widać w przykładach w tym i innych rozdziałach, umieszczamy klasy prezenterów w podkatalogach o tej samej
nazwie, na przykład prezenter Home
mapuje się na klasę App\Presentation\Home\HomePresenter
. Osiągamy
to poprzez podwojenie dwukropka (wymaga Nette Application 3.2):
application:
mapping: App\Presentation\**Presenter
Teraz przejdziemy do mapowania prezenterów na moduły. Możemy zdefiniować specyficzne mapowanie dla każdego modułu:
application:
mapping:
Front: App\Presentation\Front\**Presenter
Admin: App\Presentation\Admin\**Presenter
Api: App\Api\*Presenter
Zgodnie z tą konfiguracją, prezenter Front:Home
mapuje do klasy
App\Presentation\Front\Home\HomePresenter
, podczas gdy prezenter Api:OAuth
mapuje do klasy
App\Api\OAuthPresenter
.
Ponieważ moduły Front
i Admin
mają podobną metodę mapowania i prawdopodobnie będzie więcej
takich modułów, można utworzyć ogólną regułę, która je zastąpi. Nowa gwiazdka dla modułu zostanie dodana do
maski klasy:
application:
mapping:
*: App\Presentation\*\**Presenter
Api: App\Api\*Presenter
Działa to również w przypadku głębiej zagnieżdżonych struktur katalogów, takich jak prezenter
Admin:User:Edit
, gdzie segment z gwiazdką powtarza się dla każdego poziomu i skutkuje klasą
App\Presentation\Admin\User\Edit\EditPresenter
.
Alternatywnym zapisem jest użycie tablicy składającej się z trzech segmentów zamiast ciągu znaków. Ten zapis jest równoważny poprzedniemu:
application:
mapping:
*: [App\Presentation, *, **Presenter]
Api: [App\Api, '', *Presenter]