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 Sie die Anwendung logisch in Verzeichnisse gliedern
- wie Sie die Struktur so gestalten, dass sie mit dem Wachstum des Projekts gut skaliert
- welche möglichen Alternativen es gibt und welche Vor- oder Nachteile sie haben
Es ist wichtig zu erwähnen, dass das Nette Framework selbst keine bestimmte Struktur vorschreibt. Es ist so konzipiert, dass es sich leicht an alle Bedürfnisse und Präferenzen anpassen lässt.
Grundlegende Projektstruktur
Obwohl das Nette Framework keine feste Verzeichnisstruktur vorschreibt, gibt es eine bewährte Standardanordnung in Form des Web Project:
web-project/ ├── app/ ← Anwendungsverzeichnis ├── assets/ ← SCSS-, JS-Dateien, Bilder..., alternativ resources/ ├── bin/ ← Skripte für die Befehlszeile ├── config/ ← Konfiguration ├── log/ ← protokollierte Fehler ├── temp/ ← temporäre Dateien, Cache ├── tests/ ← Tests ├── vendor/ ← Bibliotheken, die mit Composer installiert wurden └── www/ ← öffentliches Verzeichnis (Document-Root)
Sie können diese Struktur beliebig an Ihre Bedürfnisse anpassen – Ordner umbenennen oder verschieben. Anschließend
müssen Sie nur die relativen Pfade zu den Verzeichnissen in der Datei Bootstrap.php
und gegebenenfalls
composer.json
anpassen. Mehr ist nicht nötig, keine komplexe Neukonfiguration, keine Änderung von Konstanten. Nette
verfügt über eine intelligente Autoerkennung und erkennt automatisch den Speicherort der Anwendung einschließlich ihrer
URL-Basis.
Prinzipien der Code-Organisation
Wenn Sie ein neues Projekt zum ersten Mal untersuchen, sollten Sie sich schnell darin zurechtfinden. Stellen Sie sich vor, Sie
klicken auf das Verzeichnis app/Model/
und sehen diese Struktur:
app/Model/ ├── Services/ ├── Repositories/ └── Entities/
Daraus können Sie nur entnehmen, dass das Projekt einige Dienste, Repositories und Entitäten verwendet. Über den tatsächlichen Zweck der Anwendung erfahren Sie überhaupt 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 Klassentypen) bringt in der Praxis eine Reihe von Problemen mit sich: Code, der logisch zusammenhängt, ist auf verschiedene Ordner verteilt, und Sie müssen zwischen ihnen hin- und herspringen. Deshalb werden wir nach Domänen organisieren.
Namespaces
Es ist üblich, dass die Verzeichnisstruktur mit den Namespaces in der Anwendung korrespondiert. Das bedeutet, dass der
physische Speicherort der Dateien ihrem Namespace entspricht. Zum Beispiel sollte eine Klasse, die sich in
app/Model/Product/ProductRepository.php
befindet, den Namespace App\Model\Product
haben. Dieses Prinzip
hilft bei der Orientierung 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
. Ebenso innerhalb der Anwendung: Model
, Core
,
Presentation
. Das liegt daran, dass jedes dieser Verzeichnisse ein zusammenhängendes Konzept darstellt.
Ähnlich repräsentiert z.B. app/Model/Product
alles rund um Produkte. Wir nennen es nicht Products
,
weil es sich nicht um einen Ordner voller Produkte handelt (dann wären dort Dateien wie nokia.php
,
samsung.php
). Es ist ein Namespace, der Klassen für die Arbeit mit Produkten enthält –
ProductRepository.php
, ProductService.php
.
Der Ordner app/Tasks
steht im Plural, weil er einen Satz eigenständiger ausführbarer Skripte enthält –
CleanupTask.php
, ImportTask.php
. Jedes davon ist eine eigenständige Einheit.
Zur Konsistenz empfehlen wir die Verwendung von:
- Singular für einen Namespace, der eine funktionale Einheit repräsentiert (auch wenn er mit mehreren Entitäten arbeitet)
- Plural für Sammlungen eigenständiger Einheiten
- Im Zweifelsfall oder wenn Sie nicht darüber nachdenken möchten, wählen Sie den Singular
Öffentliches Verzeichnis www/
Dieses Verzeichnis ist das einzige, das vom Web aus zugänglich ist (sog. Document-Root). Oft trifft man auch auf den Namen
public/
anstelle von www/
– das ist nur eine Frage der Konvention und hat keinen Einfluss auf die
Funktionalität des Frameworks. Das Verzeichnis enthält:
- Den Einstiegspunkt der Anwendung
index.php
- Die Datei
.htaccess
mit Regeln für mod_rewrite (bei Apache) - Statische Dateien (CSS, JavaScript, Bilder)
- Hochgeladene Dateien
Für die korrekte Sicherheit der Anwendung ist es entscheidend, den konfigurierten Document-Root richtig eingestellt zu haben.
Platzieren Sie niemals den Ordner node_modules/
in diesem Verzeichnis – er enthält Tausende von
Dateien, die ausführbar sein könnten und nicht öffentlich zugänglich sein sollten.
Anwendungsverzeichnis app/
Dies ist das Hauptverzeichnis mit dem Anwendungscode. Die Grundstruktur:
app/ ├── Core/ ← Infrastrukturangelegenheiten ├── Model/ ← Geschäftslogik ├── Presentation/ ← Presenter und Templates ├── Tasks/ ← Befehlszeilenskripte └── Bootstrap.php ← Bootstrap-Klasse der Anwendung
Bootstrap.php
ist die Startklasse der Anwendung, die
die Umgebung initialisiert, die Konfiguration lädt und den DI-Container erstellt.
Schauen wir uns nun die einzelnen Unterverzeichnisse genauer an.
Presenter und Templates
Der Präsentationsteil der Anwendung befindet sich im Verzeichnis app/Presentation
. Eine Alternative ist das kurze
app/UI
. Dies ist der Ort für alle Presenter, ihre Templates und eventuelle Hilfsklassen.
Diese Schicht organisieren wir nach Domänen. In einem komplexen Projekt, das einen E-Shop, einen Blog und eine API kombiniert, würde die Struktur so aussehen:
app/Presentation/ ├── Shop/ ← E-Shop Frontend │ ├── Product/ │ ├── Cart/ │ └── Order/ ├── Blog/ ← Blog │ ├── Home/ │ └── Post/ ├── Admin/ ← Administration │ ├── Dashboard/ │ └── Products/ └── Api/ ← API-Endpunkte └── V1/
Bei einem einfachen Blog hingegen würden wir folgende Gliederung verwenden:
app/Presentation/ ├── Front/ ← Frontend der Website │ ├── Home/ │ └── Post/ ├── Admin/ ← Administration │ ├── Dashboard/ │ └── Posts/ ├── Error/ └── Export/ ← RSS, Sitemaps etc.
Ordner wie Home/
oder Dashboard/
enthalten Presenter und Templates. Ordner wie Front/
,
Admin/
oder Api/
nennen wir Module. Technisch gesehen sind dies normale Verzeichnisse, die zur
logischen Gliederung der Anwendung dienen.
Jeder Ordner mit einem Presenter enthält den gleichnamigen Presenter und seine Templates. Zum Beispiel enthält der Ordner
Dashboard/
:
Dashboard/ ├── DashboardPresenter.php ← Presenter └── default.latte ← Template
Diese Verzeichnisstruktur spiegelt sich in den Namespaces der Klassen wider. Zum Beispiel befindet sich
DashboardPresenter
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
innerhalb des Moduls Admin
verweisen wir in der Anwendung mittels
Doppelpunktnotation als Admin:Dashboard
. Auf seine Aktion default
dann als
Admin:Dashboard:default
. Bei verschachtelten Modulen verwenden wir mehrere Doppelpunkte, zum Beispiel
Shop:Order:Detail:default
.
Flexible Strukturentwicklung
Einer der großen Vorteile dieser Struktur ist, wie elegant sie sich an die wachsenden Anforderungen des Projekts anpasst. Nehmen wir als Beispiel den Teil, der XML-Feeds generiert. 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 benötigen mehr Logik für sie… Kein Problem! Der Ordner
Export/
wird einfach zu einem Modul:
Export/ ├── Sitemap/ │ ├── SitemapPresenter.php │ └── sitemap.latte └── Feed/ ├── FeedPresenter.php ├── zbozi.latte ← Feed für Zboží.cz └── heureka.latte ← Feed für Heureka.cz
Diese Transformation ist absolut nahtlos – es genügt, neue Unterordner zu erstellen, den Code darin aufzuteilen und die
Links zu aktualisieren (z.B. von Export:feed
zu Export:Feed:zbozi
). Dadurch können wir die Struktur
nach Bedarf schrittweise erweitern, die Verschachtelungsebene ist in keiner Weise begrenzt.
Wenn Sie beispielsweise in der Administration viele Presenter haben, die sich auf die Verwaltung von Bestellungen beziehen, wie
OrderDetail
, OrderEdit
, OrderDispatch
usw., können Sie zur besseren Organisation an dieser
Stelle ein Modul (Ordner) Order
erstellen, in dem sich die (Ordner für die) Presenter Detail
,
Edit
, Dispatch
und weitere befinden.
Platzierung von Templates
In den vorherigen Beispielen haben wir gesehen, dass die Templates direkt im Ordner mit dem Presenter platziert sind:
Dashboard/ ├── DashboardPresenter.php ← Presenter ├── DashboardTemplate.php ← optionale Klasse für das Template └── default.latte ← Template
Diese Platzierung erweist sich in der Praxis als am bequemsten – Sie haben alle zusammengehörigen Dateien sofort zur Hand.
Alternativ können Sie die Templates in einem Unterordner templates/
platzieren. Nette unterstützt beide
Varianten. Sie können Templates sogar ganz außerhalb des Presentation/
-Ordners platzieren. Alles über die
Möglichkeiten zur Platzierung von Templates finden Sie im Kapitel Suche nach Templates.
Hilfsklassen und Komponenten
Zu Presentern und Templates gehören oft auch weitere Hilfsdateien. Wir platzieren sie logisch nach ihrem Wirkungsbereich:
1. Direkt beim Presenter im Falle spezifischer Komponenten für den jeweiligen Presenter:
Product/ ├── ProductPresenter.php ├── ProductGrid.php ← Komponente zur Produktauflistung └── FilterForm.php ← Formular zur Filterung
2. Für das Modul – wir empfehlen die Verwendung des Ordners Accessory
, der übersichtlich gleich am
Anfang des Alphabets platziert wird:
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/
Oder Sie können Hilfsklassen wie LatteExtension.php
oder TemplateFilters.php
im Infrastrukturordner
app/Core/Latte/
platzieren. Und Komponenten in app/Components
. Die Wahl hängt von den Gewohnheiten des
Teams ab.
Model – Das Herz der Anwendung
Das Model enthält die gesamte Geschäftslogik der Anwendung. Für seine Organisation gilt wieder die Regel – wir strukturieren nach Domänen:
app/Model/ ├── Payment/ ← alles rund um Zahlungen │ ├── PaymentFacade.php ← Hauptzugangspunkt │ ├── PaymentRepository.php │ ├── Payment.php ← Entität ├── Order/ ← alles rund um Bestellungen │ ├── OrderFacade.php │ ├── OrderRepository.php │ ├── Order.php └── Shipping/ ← alles rund um den Versand
Im Model treffen Sie typischerweise auf diese Klassentypen:
Fassaden: stellen den Hauptzugangspunkt zu einer bestimmten Domäne in der Anwendung dar. Sie fungieren als Orchestrator, der die Zusammenarbeit zwischen verschiedenen Diensten koordiniert, um vollständige Use Cases (wie “Bestellung erstellen” oder “Zahlung verarbeiten”) zu implementieren. Unter ihrer Orchestrierungsschicht verbirgt die Fassade Implementierungsdetails vor dem Rest der Anwendung und bietet so eine saubere Schnittstelle für die Arbeit mit der jeweiligen Domäne.
class OrderFacade
{
public function createOrder(Cart $cart): Order
{
// Validierung
// Erstellung der Bestellung
// Senden der E-Mail
// Eintrag in die Statistiken
}
}
Dienste: konzentrieren sich auf eine spezifische Geschäftsoperation innerhalb der Domäne. Im Gegensatz zur Fassade, die ganze Use Cases orchestriert, implementiert ein Dienst spezifische Geschäftslogik (wie Preisberechnungen oder Zahlungsverarbeitung). Dienste sind typischerweise zustandslos und können entweder von Fassaden als Bausteine für komplexere Operationen oder direkt von anderen Teilen der Anwendung für einfachere Aufgaben verwendet werden.
class PricingService
{
public function calculateTotal(Order $order): Money
{
// Preisberechnung
}
}
Repositories: stellen die gesamte Kommunikation mit dem Datenspeicher sicher, typischerweise einer Datenbank. Ihre Aufgabe ist das Laden und Speichern von Entitäten und die Implementierung von Methoden zu deren Suche. Das 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
{
}
}
Entitäten: Objekte, die die Hauptgeschäftskonzepte in der Anwendung repräsentieren, ihre eigene Identität haben und sich im Laufe der Zeit ändern. Typischerweise handelt es sich um Klassen, die mittels ORM (wie Nette Database Explorer oder Doctrine) auf Datenbanktabellen abgebildet sind. Entitäten können Geschäftsregeln enthalten, die sich auf ihre Daten beziehen, sowie Validierungslogik.
// Entität, 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 repräsentieren – beispielsweise ein Geldbetrag oder eine E-Mail-Adresse. Zwei Instanzen eines Value Objects mit gleichen Werten werden als identisch betrachtet.
Infrastrukturcode
Der Ordner Core/
(oder auch Infrastructure/
) ist die Heimat für die technische Grundlage der
Anwendung. Infrastrukturcode umfasst typischerweise:
app/Core/ ├── Router/ ← Routing und URL-Management │ └── 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 mit externen Diensten ├── Slack/ └── Stripe/
Bei kleineren Projekten genügt natürlich eine flache Gliederung:
Core/ ├── RouterFactory.php ├── Authenticator.php └── QueueMailer.php
Es handelt sich um Code, der:
- Sich um die technische Infrastruktur kümmert (Routing, Logging, Caching)
- Externe Dienste integriert (Sentry, Elasticsearch, Redis)
- Basisdienste für die gesamte Anwendung bereitstellt (Mail, Datenbank)
- Meistens unabhängig von der spezifischen Domäne ist – Cache oder Logger funktionieren gleich für E-Shop oder Blog.
Sind Sie unsicher, ob eine bestimmte Klasse hierher oder ins Modell gehört? Der Hauptunterschied besteht darin, dass der Code
in Core/
:
- Nichts über die Domäne weiß (Produkte, Bestellungen, Artikel)
- Meistens in ein anderes Projekt übertragen werden kann
- Löst “wie es funktioniert” (wie man eine E-Mail sendet), nicht “was es tut” (welche E-Mail gesendet werden soll)
Beispiel zum besseren Verständnis:
App\Core\MailerFactory
– erstellt Instanzen der Klasse zum Senden von E-Mails, kümmert sich um SMTP-EinstellungenApp\Model\OrderMailer
– verwendetMailerFactory
zum Senden von E-Mails über Bestellungen, kennt deren Templates und weiß, wann sie gesendet werden sollen
Befehlszeilenskripte
Anwendungen müssen oft Tätigkeiten außerhalb normaler HTTP-Anfragen ausführen – sei es Datenverarbeitung im Hintergrund,
Wartung oder periodische Aufgaben. Zur Ausführung dienen einfache Skripte im Verzeichnis bin/
, die eigentliche
Implementierungslogik platzieren wir dann in app/Tasks/
(oder app/Commands/
).
Beispiel:
app/Tasks/ ├── Maintenance/ ← Wartungsskripte │ ├── CleanupCommand.php ← Löschen alter Daten │ └── DbOptimizeCommand.php ← Datenbankoptimierung ├── Integration/ ← Integration mit externen Systemen │ ├── ImportProducts.php ← Import aus dem Liefersystem │ └── SyncOrders.php ← Synchronisation von Bestellungen └── Scheduled/ ← regelmäßige Aufgaben ├── NewsletterCommand.php ← Versand von Newslettern └── ReminderCommand.php ← Benachrichtigungen an Kunden
Was gehört ins Modell und was in die Befehlszeilenskripte? Zum Beispiel ist die Logik zum Senden einer einzelnen E-Mail Teil
des Modells, der Massenversand von Tausenden von E-Mails gehört bereits zu Tasks/
.
Aufgaben werden normalerweise über die Befehlszeile
ausgeführt oder über Cron. Sie können auch über eine HTTP-Anfrage gestartet werden, aber man muss an die Sicherheit
denken. Der Presenter, der die Aufgabe startet, muss abgesichert werden, zum Beispiel nur für angemeldete Benutzer oder mit einem
starken Token und Zugriff von erlaubten IP-Adressen. Bei langen Aufgaben muss das Zeitlimit des Skripts erhöht und
session_write_close()
verwendet werden, damit die Session nicht gesperrt wird.
Weitere mögliche Verzeichnisse
Neben den genannten grundlegenden Verzeichnissen können Sie je nach Projektbedarf weitere spezialisierte Ordner hinzufügen. Schauen wir uns die häufigsten davon und ihre Verwendung an:
app/ ├── Api/ ← API-Logik unabhängig von der Präsentationsschicht ├── Database/ ← Migrationsskripte und Seeder für Testdaten ├── Components/ ← gemeinsam genutzte visuelle Komponenten über die gesamte Anwendung hinweg ├── Event/ ← nützlich, wenn Sie eine ereignisgesteuerte Architektur verwenden ├── Mail/ ← E-Mail-Templates und zugehörige Logik └── Utils/ ← Hilfsklassen
Für gemeinsam genutzte visuelle Komponenten, die in Presentern über die gesamte Anwendung hinweg verwendet werden, kann der
Ordner app/Components
oder app/Controls
verwendet werden:
app/Components/ ├── Form/ ← gemeinsam genutzte Formularkomponenten │ ├── SignInForm.php │ └── UserForm.php ├── Grid/ ← Komponenten für Datenlisten │ └── DataGrid.php └── Navigation/ ← Navigationselemente ├── Breadcrumbs.php └── Menu.php
Hierher gehören Komponenten mit komplexerer Logik. Wenn Sie Komponenten zwischen mehreren Projekten teilen möchten, ist es ratsam, sie in ein separates Composer-Paket auszulagern.
Im Verzeichnis app/Mail
können Sie die Verwaltung der E-Mail-Kommunikation platzieren:
app/Mail/ ├── templates/ ← E-Mail-Templates │ ├── order-confirmation.latte │ └── welcome.latte └── OrderMailer.php
Presenter-Mapping
Das Mapping definiert Regeln zur Ableitung des Klassennamens aus dem Presenter-Namen. Wir spezifizieren sie in der Konfiguration unter dem Schlüssel
application › mapping
.
Auf dieser Seite haben wir gezeigt, dass wir Presenter im Ordner app/Presentation
(oder app/UI
)
platzieren. Diese Konvention müssen wir Nette in der Konfigurationsdatei mitteilen. Eine Zeile genügt:
application:
mapping: App\Presentation\*\**Presenter
Wie funktioniert das Mapping? Zum besseren Verständnis stellen wir uns zunächst eine Anwendung ohne Module vor. Wir möchten,
dass die Presenter-Klassen in den Namespace App\Presentation
fallen, damit der Presenter Home
auf die
Klasse App\Presentation\HomePresenter
abgebildet wird. Was wir mit dieser Konfiguration erreichen:
application:
mapping: App\Presentation\*Presenter
Das Mapping funktioniert so, dass der Presenter-Name Home
das Sternchen in der Maske
App\Presentation\*Presenter
ersetzt, wodurch wir den resultierenden Klassennamen
App\Presentation\HomePresenter
erhalten. Einfach!
Wie Sie jedoch in den Beispielen in diesem und anderen Kapiteln sehen, platzieren wir die Presenter-Klassen in gleichnamigen
Unterverzeichnissen, zum Beispiel wird der Presenter Home
auf die Klasse
App\Presentation\Home\HomePresenter
abgebildet. Dies erreichen wir mit der ***Presenter
-Maske (erfordert
Nette Application 3.2):
application:
mapping: App\Presentation\**Presenter
Nun kommen wir zum Mapping von Presentern in Module. Für jedes Modul können wir ein spezifisches Mapping definieren:
application:
mapping:
Front: App\Presentation\Front\**Presenter
Admin: App\Presentation\Admin\**Presenter
Api: App\Api\*Presenter
Gemäß 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
eine ähnliche Mapping-Methode haben und es solche Module
wahrscheinlich mehr geben wird, ist es möglich, eine allgemeine Regel zu erstellen, die sie ersetzt. Zur Klassenmaske kommt somit
ein neues Sternchen für das Modul hinzu:
application:
mapping:
*: App\Presentation\*\**Presenter
Api: App\Api\*Presenter
Es funktioniert auch für tiefer verschachtelte Verzeichnisstrukturen, wie zum Beispiel den Presenter
Admin:User:Edit
, wobei sich das Segment mit dem Sternchen für jede Ebene wiederholt und das Ergebnis die Klasse
App\Presentation\Admin\User\Edit\EditPresenter
ist.
Eine alternative Schreibweise ist die Verwendung eines Arrays anstelle einer Zeichenkette, bestehend aus drei Segmenten. Diese Schreibweise ist äquivalent zur vorherigen:
application:
mapping:
*: [App\Presentation, *, **Presenter]
Api: [App\Api, '', *Presenter]