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 .htaccess mit 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-Einstellungen
  • App\Model\OrderMailer – nutzt MailerFactory, 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]
Version: 4.x