Verzeichnisstruktur der Anwendung
Wie entwirft man eine übersichtliche und skalierbare Verzeichnisstruktur für Projekte im Nette Framework? Wir zeigen Ihnen bewährte Praktiken, die Ihnen bei der Organisation Ihres Codes helfen. Sie erfahren:
- wie man die Anwendung logisch in Verzeichnisse gliedert
- wie man die Struktur so entwirft, dass sie mit wachsendem Projekt gut skaliert
- welche möglichen Alternativen es gibt und welche Vor- und Nachteile sie haben
Wichtig ist zu erwähnen, dass das Nette Framework selbst keine bestimmte Struktur erzwingt. Es ist so gestaltet, dass es sich leicht an beliebige Bedürfnisse und Vorlieben anpassen lässt.
Grundlegende Projektstruktur
Auch wenn das Nette Framework keine feste Verzeichnisstruktur vorschreibt, gibt es eine bewährte Standardaufteilung in Form des Web Project:
web-project/ ├── app/ ← Verzeichnis der Anwendung ├── assets/ ← SCSS-, JS-Dateien, Bilder…, alternativ resources/ ├── bin/ ← Skripte für die Kommandozeile ├── config/ ← Konfiguration ├── log/ ← protokollierte Fehler ├── temp/ ← temporäre Dateien, Cache ├── tests/ ← Tests ├── vendor/ ← von Composer installierte Bibliotheken └── www/ ← öffentliches Verzeichnis (Document-Root)
Sie können diese Struktur nach Ihren Bedürfnissen frei anpassen – Ordner umbenennen oder verschieben. Danach müssen Sie
lediglich die relativen Pfade zu den Verzeichnissen in Bootstrap.php und gegebenenfalls in composer.json
anpassen. Mehr ist nicht nötig, keine komplizierte Neukonfiguration, keine Änderungen an Konstanten. Nette hat eine intelligente
Autodetection und erkennt den Ort der Anwendung samt ihrer Basis-URL automatisch.
Prinzipien der Code-Organisation
Wenn Sie ein neues Projekt zum ersten Mal erkunden, sollten Sie sich schnell zurechtfinden. Stellen Sie sich vor, Sie klicken
auf das Verzeichnis app/Model/ und sehen diese Struktur:
app/Model/ ├── Services/ ├── Repositories/ └── Entities/
Daraus erfahren Sie nur, dass das Projekt irgendwelche Services, Repositories und Entities verwendet. Über den eigentlichen Zweck der Anwendung erfahren Sie nichts.
Schauen wir uns einen anderen Ansatz an – die Organisation nach Domänen:
app/Model/ ├── Cart/ ├── Payment/ ├── Order/ └── Product/
Hier ist es anders – auf den ersten Blick ist klar, dass es sich um einen E-Shop handelt. Schon die Verzeichnisnamen verraten, was die Anwendung kann: Sie arbeitet mit Zahlungen, Bestellungen und Produkten.
Der erste Ansatz (Organisation nach Klassentyp) bringt in der Praxis mehrere Probleme mit sich: Logisch zusammengehöriger Code ist über verschiedene Ordner verstreut, und Sie müssen zwischen ihnen hin- und herspringen. Deshalb organisieren wir nach Domänen.
Namespaces
Es ist üblich, dass die Verzeichnisstruktur den Namespaces in der Anwendung entspricht. Das bedeutet, dass der physische Ort
der Dateien ihrem Namespace entspricht. Eine Klasse in app/Model/Product/ProductRepository.php sollte zum Beispiel
den Namespace App\Model\Product haben. Dieses Prinzip hilft beim Navigieren im Code und vereinfacht das
Autoloading.
Singular vs. Plural in Namen
Beachten Sie, dass wir für die Hauptverzeichnisse der Anwendung den Singular verwenden: app, config,
log, temp, www. Dasselbe gilt innerhalb der Anwendung: Model,
Core, Presentation. Der Grund ist, dass jedes davon ein einziges zusammenhängendes Konzept
darstellt.
Ebenso stellt app/Model/Product alles dar, was mit Produkten zu tun hat. Wir nennen es nicht
Products, weil es kein Ordner voller Produkte ist (der würde Dateien wie nokia.php,
samsung.php enthalten). Es ist ein Namespace mit Klassen für die Arbeit mit Produkten –
ProductRepository.php, ProductService.php.
Der Ordner app/Tasks steht im Plural, weil er eine Menge eigenständiger ausführbarer Skripte enthält –
CleanupTask.php, ImportTask.php. Jedes davon ist eine unabhängige Einheit.
Aus Gründen der Konsistenz empfehlen wir:
- Singular für Namespaces, die eine funktionale Einheit darstellen (auch wenn sie mit mehreren Entities arbeiten)
- Plural für Sammlungen unabhängiger Einheiten
- Im Zweifel, oder wenn Sie nicht darüber nachdenken wollen, wählen Sie den Singular
Öffentliches Verzeichnis www/
Dieses Verzeichnis ist das einzige, das aus dem Web erreichbar ist (der Document-Root). Häufig begegnet Ihnen statt
www/ der Name public/ – das ist reine Konventionssache und hat keinen Einfluss auf die Funktion der
Anwendung. Das Verzeichnis enthält:
- den Einstiegspunkt der Anwendung
index.php - die Datei
.htaccessmit den Regeln für mod_rewrite (für Apache) - statische Dateien (CSS, JavaScript, Bilder)
- hochgeladene Dateien
Für die richtige Absicherung der Anwendung ist es entscheidend, den Document-Root korrekt zu konfigurieren.
Legen Sie den Ordner node_modules/ niemals in dieses Verzeichnis – er enthält Tausende von
Dateien, die ausführbar sein können und nicht öffentlich zugänglich sein sollten.
Verzeichnis der Anwendung app/
Das ist das Hauptverzeichnis mit dem Code der Anwendung. Grundstruktur:
app/ ├── Core/ ← infrastrukturelle Belange ├── Model/ ← Geschäftslogik ├── Presentation/ ← Presenter und Templates ├── Tasks/ ← Kommandoskripte └── Bootstrap.php ← Startklasse der Anwendung
Bootstrap.php ist die Startklasse der Anwendung,
die die Umgebung initialisiert, die Konfiguration lädt und den DI-Container erzeugt.
Schauen wir uns nun die einzelnen Unterverzeichnisse genauer an.
Presenter und Templates
Der Präsentationsteil der Anwendung liegt im Verzeichnis app/Presentation. Eine Alternative ist das kürzere
app/UI. Das ist der Ort für alle Presenter, ihre Templates und eventuelle zugehörige Hilfsklassen.
Diese Schicht organisieren wir nach Domänen. In einem komplexen Projekt, das E-Shop, Blog und API vereint, sähe die Struktur so aus:
app/Presentation/ ├── Shop/ ← Frontend des E-Shops │ ├── Product/ │ ├── Cart/ │ └── Order/ ├── Blog/ ← Blog │ ├── Home/ │ └── Post/ ├── Admin/ ← Administration │ ├── Dashboard/ │ └── Products/ └── Api/ ← API-Endpunkte └── V1/
Für einen einfachen Blog würden wir dagegen folgende Struktur verwenden:
app/Presentation/ ├── Front/ ← Frontend der Website │ ├── Home/ │ └── Post/ ├── Admin/ ← Administration │ ├── Dashboard/ │ └── Posts/ ├── Error/ └── Export/ ← RSS, Sitemaps usw.
Ordner wie Home/ oder Dashboard/ enthalten Presenter und Templates. Ordner wie Front/,
Admin/ oder Api/ nennt man Module. Technisch sind das gewöhnliche Verzeichnisse, die der
logischen Aufteilung der Anwendung dienen.
Jeder Ordner mit einem Presenter enthält die Presenter-Datei selbst und ihre Templates. Der Ordner Dashboard/
enthält zum Beispiel:
Dashboard/ ├── DashboardPresenter.php ← Presenter └── default.latte ← Template
Diese Verzeichnisstruktur spiegelt sich in den Namespaces der Klassen wider. DashboardPresenter liegt zum Beispiel
im Namespace App\Presentation\Admin\Dashboard (siehe Presenter-Mapping):
namespace App\Presentation\Admin\Dashboard;
class DashboardPresenter extends Nette\Application\UI\Presenter
{
// ...
}
Auf den Presenter Dashboard im Modul Admin verweisen wir in der Anwendung mit der
Doppelpunkt-Schreibweise als Admin:Dashboard. Seine Aktion default wird dann als
Admin:Dashboard:default bezeichnet. Bei verschachtelten Modulen verwenden wir mehrere Doppelpunkte, zum Beispiel
Shop:Order:Detail:default.
Flexible Entwicklung der Struktur
Einer der großen Vorteile dieser Struktur ist, wie elegant sie sich an wachsende Anforderungen des Projekts anpasst. Nehmen wir als Beispiel den Teil, der XML-Feeds erzeugt. Am Anfang haben wir eine einfache Form:
Export/ ├── ExportPresenter.php ← ein Presenter für alle Exporte ├── sitemap.latte ← Template für die Sitemap └── feed.latte ← Template für den RSS-Feed
Mit der Zeit kommen weitere Feed-Typen hinzu, und wir brauchen mehr Logik dafür … Kein Problem! Der Ordner
Export/ wird einfach zu einem Modul:
Export/ ├── Sitemap/ │ ├── SitemapPresenter.php │ └── sitemap.latte └── Feed/ ├── FeedPresenter.php ├── amazon.latte ← Feed für Amazon └── ebay.latte ← Feed für eBay
Diese Umwandlung verläuft völlig reibungslos – legen Sie einfach neue Unterordner an, verteilen Sie den Code darauf und
aktualisieren Sie die Links (z. B. von Export:feed auf Export:Feed:amazon). Dadurch können wir die
Struktur nach Bedarf schrittweise erweitern, die Verschachtelungstiefe ist in keiner Weise begrenzt.
Wenn Sie zum Beispiel in der Administration viele Presenter rund um die Verwaltung von Bestellungen haben, etwa
OrderDetail, OrderEdit, OrderDispatch usw., können Sie zur besseren Organisation ein Modul
(einen Ordner) namens Order anlegen, das (Ordner für) die Presenter Detail, Edit,
Dispatch und weitere enthält.
Ort der Templates
In den vorherigen Beispielen haben wir gesehen, dass die Templates direkt im Ordner mit dem Presenter liegen:
Dashboard/ ├── DashboardPresenter.php ← Presenter ├── DashboardTemplate.php ← optionale Template-Klasse └── default.latte ← Template
Dieser Ort erweist sich in der Praxis als der bequemste – Sie haben alle zusammengehörigen Dateien griffbereit.
Alternativ können Sie die Templates in einen Unterordner templates/ legen. Nette unterstützt beide Varianten.
Sie können die Templates sogar vollständig außerhalb des Ordners Presentation/ ablegen. Alles über die
Möglichkeiten des Template-Orts finden Sie im Kapitel Suche nach Templates.
Hilfsklassen und Komponenten
Zu Presentern und Templates kommen oft weitere Hilfsdateien. Wir platzieren sie logisch nach ihrem Geltungsbereich:
1. Direkt beim Presenter im Fall von Komponenten, die für diesen Presenter spezifisch sind:
Product/ ├── ProductPresenter.php ├── ProductGrid.php ← Komponente für die Produktauflistung └── FilterForm.php ← Formular zum Filtern
2. Für das Modul – wir empfehlen den Ordner Accessory, der alphabetisch praktischerweise ganz am
Anfang steht:
Front/ ├── Accessory/ │ ├── NavbarControl.php ← Komponenten für das Frontend │ └── TemplateFilters.php ├── Product/ └── Cart/
3. Für die gesamte Anwendung – in Presentation/Accessory/:
app/Presentation/ ├── Accessory/ │ ├── LatteExtension.php │ └── TemplateFilters.php ├── Front/ └── Admin/
Alternativ können Sie Hilfsklassen wie LatteExtension.php oder TemplateFilters.php in den
Infrastrukturordner app/Core/Latte/ legen. Und Komponenten nach app/Components. Die Wahl hängt von den
Konventionen im Team ab.
Model – Herz der Anwendung
Das Model enthält die gesamte Geschäftslogik der Anwendung. Die Regel für seine Organisation lautet wieder: Struktur nach Domänen:
app/Model/ ├── Payment/ ← alles rund um Zahlungen │ ├── PaymentFacade.php ← Haupteinstiegspunkt │ ├── PaymentRepository.php │ ├── Payment.php ← Entity ├── Order/ ← alles rund um Bestellungen │ ├── OrderFacade.php │ ├── OrderRepository.php │ ├── Order.php └── Shipping/ ← alles rund um den Versand
Im Model begegnen Ihnen typischerweise diese Arten von Klassen:
Facades: Sie stellen den Haupteinstiegspunkt in eine bestimmte Domäne der Anwendung dar. Sie treten als Orchestrator auf und koordinieren die Zusammenarbeit verschiedener Services, um vollständige Use-Cases umzusetzen (etwa “Bestellung anlegen” oder “Zahlung verarbeiten”). Unterhalb ihrer Orchestrierungsschicht verbirgt die Facade Implementierungsdetails vor dem Rest der Anwendung und bietet damit eine saubere Schnittstelle für die Arbeit mit der jeweiligen Domäne.
class OrderFacade
{
public function createOrder(Cart $cart): Order
{
// Validierung
// Anlegen der Bestellung
// Versand der E-Mail
// Schreiben in die Statistik
}
}
Services: Sie konzentrieren sich auf konkrete Geschäftsoperationen innerhalb einer Domäne. Anders als Facades, die ganze Use-Cases orchestrieren, implementiert ein Service konkrete Geschäftslogik (etwa Preisberechnungen oder die Verarbeitung von Zahlungen). Services sind typischerweise zustandslos und lassen sich entweder von Facades als Bausteine für komplexere Operationen oder für einfachere Aufgaben direkt von anderen Teilen der Anwendung verwenden.
class PricingService
{
public function calculateTotal(Order $order): Money
{
// Preisberechnung
}
}
Repositories: Sie kümmern sich um die gesamte Kommunikation mit dem Datenspeicher, typischerweise einer Datenbank. Ihre Aufgabe ist es, Entities zu laden und zu speichern und Methoden zu ihrer Suche zu implementieren. Ein Repository schirmt den Rest der Anwendung von den Implementierungsdetails der Datenbank ab und bietet eine objektorientierte Schnittstelle für die Arbeit mit Daten.
class OrderRepository
{
public function find(int $id): ?Order
{
}
public function findByCustomer(int $customerId): array
{
}
}
Entities: Objekte, die die wichtigsten Geschäftskonzepte der Anwendung darstellen, eine eigene Identität haben und sich im Laufe der Zeit ändern. Typischerweise sind das Klassen, die per ORM (etwa Nette Database Explorer oder Doctrine) auf Datenbanktabellen abgebildet werden. Entities können Geschäftsregeln zu ihren Daten und Validierungslogik enthalten.
// Entity, die auf die Datenbanktabelle 'orders' abgebildet ist
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: unveränderliche Objekte, die Werte ohne eigene Identität darstellen – zum Beispiel einen Geldbetrag oder eine E-Mail-Adresse. Zwei Instanzen eines Value Objects mit denselben Werten gelten als identisch.
Infrastrukturcode
Der Ordner Core/ (alternativ Infrastructure/) ist die Heimat der technischen Grundlage der Anwendung.
Zum Infrastrukturcode gehören typischerweise:
app/Core/ ├── Router/ ← Routing und URL-Verwaltung │ └── RouterFactory.php ├── Security/ ← Authentifizierung und Autorisierung │ ├── Authenticator.php │ └── Authorizator.php ├── Logging/ ← Logging und Monitoring │ ├── SentryLogger.php │ └── FileLogger.php ├── Cache/ ← Caching-Schicht │ └── FullPageCache.php └── Integration/ ← Integration externer Dienste ├── Slack/ └── Stripe/
Für kleinere Projekte genügt natürlich eine flache Struktur:
Core/ ├── RouterFactory.php ├── Authenticator.php └── QueueMailer.php
Das ist Code, der:
- sich um die technische Infrastruktur kümmert (Routing, Logging, Caching)
- externe Dienste integriert (Sentry, Elasticsearch, Redis)
- grundlegende Services für die gesamte Anwendung bereitstellt (Mail, Datenbank)
- meist unabhängig von einer konkreten Domäne ist – Cache oder Logger funktionieren für einen E-Shop genauso wie für einen Blog.
Sie fragen sich, ob eine bestimmte Klasse hierher oder ins Model gehört? Der entscheidende Unterschied ist, dass Code in
Core/:
- nichts über die Domäne weiß (Produkte, Bestellungen, Artikel)
- sich meist in ein anderes Projekt übertragen lässt
- löst “wie es funktioniert” (wie man eine E-Mail versendet), nicht “was es tut” (welche E-Mail zu versenden ist)
Ein Beispiel zum besseren Verständnis:
App\Core\MailerFactory– erzeugt Instanzen der Klasse zum Versenden von E-Mails, kümmert sich um die SMTP-EinstellungenApp\Model\OrderMailer– nutztMailerFactory, um E-Mails zu Bestellungen zu versenden, kennt ihre Templates und weiß, wann sie versendet werden sollen
Kommandoskripte
Anwendungen müssen oft Tätigkeiten außerhalb der gewöhnlichen HTTP-Requests ausführen – sei es Datenverarbeitung im
Hintergrund, Wartung oder periodische Aufgaben. Zur Ausführung dienen einfache Skripte im Verzeichnis bin/, während
die eigentliche Implementierungslogik in app/Tasks/ (oder app/Commands/) liegt.
Beispiel:
app/Tasks/ ├── Maintenance/ ← Wartungsskripte │ ├── CleanupCommand.php ← Löschen alter Daten │ └── DbOptimizeCommand.php ← Optimierung der Datenbank ├── Integration/ ← Integration externer Systeme │ ├── ImportProducts.php ← Import aus dem System des Lieferanten │ └── SyncOrders.php ← Synchronisation der Bestellungen └── Scheduled/ ← regelmäßige Aufgaben ├── NewsletterCommand.php ← Versand von Newslettern └── ReminderCommand.php ← Benachrichtigungen an Kunden
Was gehört ins Model und was in Kommandoskripte? Die Logik zum Versenden einer einzelnen E-Mail ist zum Beispiel Teil des
Models, während der Massenversand Tausender E-Mails in Tasks/ gehört.
Tasks werden üblicherweise von der Kommandozeile oder per Cron ausgeführt: Das Skript in bin/ erzeugt mit der
Methode bootConsoleApplication() den
DI-Container und holt den benötigten Service aus ihm. Sie lassen sich auch über einen HTTP-Request ausführen, dabei muss aber
an die Sicherheit gedacht werden. Der Presenter, der den Task ausführt, muss abgesichert werden, zum Beispiel nur für
angemeldete Benutzer oder mit einem starken Token und Zugriff von erlaubten IP-Adressen. Bei lang laufenden Tasks muss das
Zeitlimit des Skripts erhöht und session_write_close() verwendet werden, um die Session nicht zu blockieren.
Weitere mögliche Verzeichnisse
Neben den genannten Grundverzeichnissen können Sie je nach Bedarf des Projekts weitere spezialisierte Ordner ergänzen. Schauen wir uns die häufigsten und ihre Verwendung an:
app/ ├── Api/ ← API-Logik unabhängig von der Präsentationsschicht ├── Database/ ← Migrationsskripte und Seeder für Testdaten ├── Components/ ← gemeinsame visuelle Komponenten der gesamten Anwendung ├── Event/ ← nützlich bei einer ereignisgesteuerten Architektur ├── Mail/ ← E-Mail-Templates und zugehörige Logik └── Utils/ ← Hilfsklassen
Für gemeinsame visuelle Komponenten, die in Presentern der gesamten Anwendung verwendet werden, können Sie den Ordner
app/Components oder app/Controls nutzen:
app/Components/ ├── Form/ ← gemeinsame Formularkomponenten │ ├── SignInForm.php │ └── UserForm.php ├── Grid/ ← Komponenten für Datenauflistungen │ └── DataGrid.php └── Navigation/ ← Navigationselemente ├── Breadcrumbs.php └── Menu.php
Hierher gehören Komponenten mit komplexerer Logik. Wollen Sie Komponenten zwischen mehreren Projekten teilen, empfiehlt es sich, sie in ein eigenes Composer-Paket auszulagern.
Im Verzeichnis app/Mail können Sie die Verwaltung der E-Mail-Kommunikation unterbringen:
app/Mail/ ├── templates/ ← E-Mail-Templates │ ├── order-confirmation.latte │ └── welcome.latte └── OrderMailer.php
Presenter-Mapping
Das Mapping definiert die Regeln, nach denen aus dem Namen des Presenters der Klassenname abgeleitet wird. Wir geben sie in der
Konfiguration unter dem Schlüssel
application › mapping an.
Auf dieser Seite haben wir gezeigt, dass wir Presenter im Ordner app/Presentation (oder app/UI)
ablegen. Seit Nette Application 3.3 ist das die Standardkonvention, die nicht konfiguriert werden muss. Wenn Sie eine andere
Struktur verwenden oder das Mapping ausdrücklich angeben wollen, entspricht die Standardeinstellung dieser Zeile:
application:
mapping: App\Presentation\*\**Presenter
Wie funktioniert das Mapping? Zum besseren Verständnis stellen wir uns zunächst eine Anwendung ohne Module vor. Wir wollen,
dass die Presenter-Klassen unter den Namespace App\Presentation fallen, sodass der Presenter Home auf
die Klasse App\Presentation\HomePresenter abgebildet wird. Das erreichen wir mit dieser Konfiguration:
application:
mapping: App\Presentation\*Presenter
Das Mapping funktioniert so, dass der Stern in der Maske App\Presentation\*Presenter durch den Namen des
Presenters Home ersetzt wird, woraus der endgültige Klassenname App\Presentation\HomePresenter
entsteht. Einfach!
Wie Sie jedoch in den Beispielen in diesem und anderen Kapiteln sehen, legen wir die Presenter-Klassen in gleichnamige
Unterverzeichnisse, der Presenter Home wird also auf die Klasse App\Presentation\Home\HomePresenter
abgebildet. Das erreichen wir mit dem doppelten Stern ** (erfordert Nette Application 3.2.3):
application:
mapping: App\Presentation\**Presenter
Nun gehen wir zum Mapping von Presentern in Module über. Für jedes Modul können wir ein eigenes Mapping definieren:
application:
mapping:
Front: App\Presentation\Front\**Presenter
Admin: App\Presentation\Admin\**Presenter
Api: App\Api\*Presenter
Nach dieser Konfiguration wird der Presenter Front:Home auf die Klasse
App\Presentation\Front\Home\HomePresenter abgebildet, während der Presenter Api:OAuth auf die Klasse
App\Api\OAuthPresenter abgebildet wird.
Da die Module Front und Admin ein ähnliches Mapping-Muster haben und es davon vermutlich noch mehr
geben wird, lässt sich eine allgemeine Regel erstellen, die sie ersetzt. In die Klassenmaske kommt ein neuer Stern für das
Modul hinzu:
application:
mapping:
*: App\Presentation\*\**Presenter
Api: App\Api\*Presenter
Es funktioniert auch für tiefer verschachtelte Verzeichnisstrukturen, etwa den Presenter Admin:User:Edit, bei dem
sich das Segment mit dem Stern für jede Modulebene wiederholt, woraus die Klasse
App\Presentation\Admin\User\Edit\EditPresenter entsteht.
Eine alternative Schreibweise ist, statt eines Strings ein Array aus drei Segmenten zu verwenden. Für die oben gezeigten Beispiele ist diese Schreibweise gleichwertig zur vorherigen:
application:
mapping:
*: [App\Presentation, *, **Presenter]
Api: [App\Api, '', *Presenter]