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-Einstellungen
  • App\Model\OrderMailer – verwendet MailerFactory 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]
Version: 4.0