Structure des répertoires de l'application

Comment concevoir une structure de répertoires claire et évolutive pour les projets Nette Framework ? Nous vous montrerons les meilleures pratiques qui vous aideront à organiser votre code. Vous apprendrez :

  • comment diviser logiquement l'application en répertoires
  • comment concevoir la structure pour qu'elle évolue bien avec la croissance du projet
  • quelles sont les alternatives possibles et leurs avantages ou inconvénients

Il est important de mentionner que Nette Framework lui-même n'impose aucune structure spécifique. Il est conçu pour être facilement adaptable à tous les besoins et préférences.

Structure de base du projet

Bien que Nette Framework ne dicte aucune structure de répertoires fixe, il existe une disposition par défaut éprouvée sous la forme du Web Project :

web-project/
├── app/              ← répertoire de l'application
├── assets/           ← fichiers SCSS, JS, images..., alternativement resources/
├── bin/              ← scripts pour la ligne de commande
├── config/           ← configuration
├── log/              ← erreurs journalisées
├── temp/             ← fichiers temporaires, cache
├── tests/            ← tests
├── vendor/           ← bibliothèques installées par Composer
└── www/              ← répertoire public (document-root)

Vous pouvez modifier cette structure librement selon vos besoins – renommer ou déplacer des dossiers. Ensuite, il suffit de modifier les chemins relatifs vers les répertoires dans le fichier Bootstrap.php et éventuellement composer.json. Rien de plus n'est nécessaire, pas de reconfiguration complexe, pas de changement de constantes. Nette dispose d'une autodétection intelligente et reconnaît automatiquement l'emplacement de l'application, y compris sa base d'URL.

Principes d'organisation du code

Lorsque vous explorez un nouveau projet pour la première fois, vous devriez pouvoir vous y orienter rapidement. Imaginez que vous ouvrez le répertoire app/Model/ et voyez cette structure :

app/Model/
├── Services/
├── Repositories/
└── Entities/

Vous en déduisez seulement que le projet utilise des services, des dépôts et des entités. Vous n'apprenez rien sur le but réel de l'application.

Regardons une autre approche – l'organisation par domaines :

app/Model/
├── Cart/
├── Payment/
├── Order/
└── Product/

Ici, c'est différent – au premier coup d'œil, il est clair qu'il s'agit d'une boutique en ligne. Les noms mêmes des répertoires révèlent ce que l'application fait – elle travaille avec les paiements, les commandes et les produits.

La première approche (organisation par type de classe) pose en pratique de nombreux problèmes : le code qui est logiquement lié est dispersé dans différents dossiers et vous devez passer de l'un à l'autre. C'est pourquoi nous organiserons par domaines.

Espaces de noms

Il est d'usage que la structure des répertoires corresponde aux espaces de noms dans l'application. Cela signifie que l'emplacement physique des fichiers correspond à leur namespace. Par exemple, une classe située dans app/Model/Product/ProductRepository.php devrait avoir le namespace App\Model\Product. Ce principe aide à s'orienter dans le code et simplifie l'autoloading.

Singulier vs pluriel dans les noms

Notez que pour les répertoires principaux de l'application, nous utilisons le singulier : app, config, log, temp, www. De même à l'intérieur de l'application : Model, Core, Presentation. C'est parce que chacun d'eux représente un concept cohérent unique.

De même, par exemple, app/Model/Product représente tout ce qui concerne les produits. Nous ne l'appellerons pas Products, car ce n'est pas un dossier plein de produits (il y aurait des fichiers nokia.php, samsung.php). C'est un namespace contenant des classes pour travailler avec les produits – ProductRepository.php, ProductService.php.

Le dossier app/Tasks est au pluriel car il contient un ensemble de scripts exécutables distincts – CleanupTask.php, ImportTask.php. Chacun d'eux est une unité distincte.

Pour la cohérence, nous recommandons d'utiliser :

  • Le singulier pour un namespace représentant une unité fonctionnelle (même s'il travaille avec plusieurs entités)
  • Le pluriel pour les collections d'unités distinctes
  • En cas d'incertitude ou si vous ne voulez pas y penser, choisissez le singulier

Répertoire public www/

Ce répertoire est le seul accessible depuis le web (appelé document-root). Vous pouvez souvent rencontrer le nom public/ au lieu de www/ – c'est juste une question de convention et cela n'affecte pas la fonctionnalité du framework. Le répertoire contient :

  • Le point d'entrée de l'application index.php
  • Le fichier .htaccess avec les règles pour mod_rewrite (pour Apache)
  • Les fichiers statiques (CSS, JavaScript, images)
  • Les fichiers uploadés

Pour une sécurité correcte de l'application, il est essentiel d'avoir le document-root correctement configuré.

Ne placez jamais le dossier node_modules/ dans ce répertoire – il contient des milliers de fichiers qui peuvent être exécutables et ne devraient pas être accessibles publiquement.

Répertoire applicatif app/

C'est le répertoire principal contenant le code de l'application. Structure de base :

app/
├── Core/               ← questions d'infrastructure
├── Model/              ← logique métier
├── Presentation/       ← presenters et templates
├── Tasks/              ← scripts de commande
└── Bootstrap.php       ← classe de démarrage de l'application

Bootstrap.php est la classe de démarrage de l'application qui initialise l'environnement, charge la configuration et crée le conteneur DI.

Examinons maintenant plus en détail les différents sous-répertoires.

Presenters et templates

La partie présentation de l'application se trouve dans le répertoire app/Presentation. Une alternative est le court app/UI. C'est l'endroit pour tous les presenters, leurs templates et d'éventuelles classes auxiliaires.

Nous organisons cette couche par domaines. Dans un projet complexe combinant une boutique en ligne, un blog et une API, la structure ressemblerait à ceci :

app/Presentation/
├── Shop/              ← frontend de la boutique en ligne
│   ├── Product/
│   ├── Cart/
│   └── Order/
├── Blog/              ← blog
│   ├── Home/
│   └── Post/
├── Admin/             ← administration
│   ├── Dashboard/
│   └── Products/
└── Api/               ← points de terminaison API
	└── V1/

Inversement, pour un blog simple, nous utiliserions la division suivante :

app/Presentation/
├── Front/             ← frontend du site
│   ├── Home/
│   └── Post/
├── Admin/             ← administration
│   ├── Dashboard/
│   └── Posts/
├── Error/
└── Export/            ← RSS, sitemaps etc.

Les dossiers comme Home/ ou Dashboard/ contiennent les presenters et les templates. Les dossiers comme Front/, Admin/ ou Api/ sont appelés modules. Techniquement, ce sont des répertoires normaux qui servent à la division logique de l'application.

Chaque dossier avec un presenter contient un presenter du même nom et ses templates. Par exemple, le dossier Dashboard/ contient :

Dashboard/
├── DashboardPresenter.php     ← presenter
└── default.latte              ← template

Cette structure de répertoires se reflète dans les espaces de noms des classes. Par exemple, DashboardPresenter se trouve dans le namespace App\Presentation\Admin\Dashboard (voir mapování presenterů) :

namespace App\Presentation\Admin\Dashboard;

class DashboardPresenter extends Nette\Application\UI\Presenter
{
	// ...
}

Nous faisons référence au presenter Dashboard à l'intérieur du module Admin dans l'application en utilisant la notation à deux points comme Admin:Dashboard. À son action default ensuite comme Admin:Dashboard:default. En cas de modules imbriqués, nous utilisons plus de deux points, par exemple Shop:Order:Detail:default.

Développement flexible de la structure

L'un des grands avantages de cette structure est la manière élégante dont elle s'adapte aux besoins croissants du projet. Prenons comme exemple la partie générant les flux XML. Au début, nous avons une forme simple :

Export/
├── ExportPresenter.php   ← un presenter pour toutes les exportations
├── sitemap.latte         ← template pour le sitemap
└── feed.latte            ← template pour le flux RSS

Avec le temps, d'autres types de flux sont ajoutés et nous avons besoin de plus de logique pour eux… Pas de problème ! Le dossier Export/ devient simplement un module :

Export/
├── Sitemap/
│   ├── SitemapPresenter.php
│   └── sitemap.latte
└── Feed/
	├── FeedPresenter.php
	├── zbozi.latte         ← flux pour Zboží.cz
	└── heureka.latte       ← flux pour Heureka.cz

Cette transformation est absolument fluide – il suffit de créer de nouveaux sous-dossiers, d'y répartir le code et de mettre à jour les liens (par exemple de Export:feed à Export:Feed:zbozi). Grâce à cela, nous pouvons étendre progressivement la structure selon les besoins, le niveau d'imbrication n'est pas limité.

Si, par exemple, dans l'administration, vous avez de nombreux presenters concernant la gestion des commandes, tels que OrderDetail, OrderEdit, OrderDispatch, etc., vous pouvez créer un module (dossier) Order à cet endroit pour une meilleure organisation, qui contiendra (les dossiers pour) les presenters Detail, Edit, Dispatch et autres.

Emplacement des templates

Dans les exemples précédents, nous avons vu que les templates sont placés directement dans le dossier avec le presenter :

Dashboard/
├── DashboardPresenter.php     ← presenter
├── DashboardTemplate.php      ← classe optionnelle pour le template
└── default.latte              ← template

Cet emplacement s'avère le plus pratique en pratique – vous avez tous les fichiers associés à portée de main.

Alternativement, vous pouvez placer les templates dans un sous-dossier templates/. Nette prend en charge les deux variantes. Vous pouvez même placer les templates complètement en dehors du dossier Presentation/. Tout sur les options d'emplacement des templates se trouve dans le chapitre Recherche de templates.

Classes auxiliaires et composants

Aux presenters et templates s'ajoutent souvent d'autres fichiers auxiliaires. Nous les plaçons logiquement en fonction de leur portée :

1. Directement avec le presenter dans le cas de composants spécifiques pour ce presenter :

Product/
├── ProductPresenter.php
├── ProductGrid.php        ← composant pour l'affichage des produits
└── FilterForm.php         ← formulaire pour le filtrage

2. Pour le module – nous recommandons d'utiliser le dossier Accessory, qui se place de manière claire au début de l'alphabet :

Front/
├── Accessory/
│   ├── NavbarControl.php    ← composants pour le frontend
│   └── TemplateFilters.php
├── Product/
└── Cart/

3. Pour toute l'application – dans Presentation/Accessory/ :

app/Presentation/
├── Accessory/
│   ├── LatteExtension.php
│   └── TemplateFilters.php
├── Front/
└── Admin/

Ou vous pouvez placer les classes auxiliaires comme LatteExtension.php ou TemplateFilters.php dans le dossier d'infrastructure app/Core/Latte/. Et les composants dans app/Components. Le choix dépend des habitudes de l'équipe.

Modèle – cœur de l'application

Le modèle contient toute la logique métier de l'application. Pour son organisation, la règle s'applique à nouveau – nous structurons par domaines :

app/Model/
├── Payment/                   ← tout ce qui concerne les paiements
│   ├── PaymentFacade.php      ← point d'entrée principal
│   ├── PaymentRepository.php
│   ├── Payment.php            ← entité
├── Order/                     ← tout ce qui concerne les commandes
│   ├── OrderFacade.php
│   ├── OrderRepository.php
│   ├── Order.php
└── Shipping/                  ← tout ce qui concerne la livraison

Dans le modèle, vous rencontrerez généralement ces types de classes :

Façades: représentent le point d'entrée principal dans un domaine spécifique de l'application. Elles agissent comme un orchestrateur qui coordonne la coopération entre différents services afin d'implémenter des cas d'utilisation complets (comme “créer une commande” ou “traiter un paiement”). Sous sa couche d'orchestration, la façade masque les détails d'implémentation au reste de l'application, offrant ainsi une interface propre pour travailler avec le domaine donné.

class OrderFacade
{
	public function createOrder(Cart $cart): Order
	{
		// validation
		// création de la commande
		// envoi d'e-mail
		// écriture dans les statistiques
	}
}

Services: se concentrent sur une opération métier spécifique au sein du domaine. Contrairement à la façade, qui orchestre des cas d'utilisation entiers, un service implémente une logique métier spécifique (comme le calcul des prix ou le traitement des paiements). Les services sont typiquement sans état et peuvent être utilisés soit par les façades comme blocs de construction pour des opérations plus complexes, soit directement par d'autres parties de l'application pour des tâches plus simples.

class PricingService
{
	public function calculateTotal(Order $order): Money
	{
		// calcul du prix
	}
}

Dépôts (Repositories): assurent toute la communication avec le stockage de données, typiquement une base de données. Leur tâche est de charger et de sauvegarder des entités et d'implémenter des méthodes pour leur recherche. Le dépôt isole le reste de l'application des détails d'implémentation de la base de données et fournit une interface orientée objet pour travailler avec les données.

class OrderRepository
{
	public function find(int $id): ?Order
	{
	}

	public function findByCustomer(int $customerId): array
	{
	}
}

Entités: objets représentant les principaux concepts métier dans l'application, qui ont leur propre identité et changent avec le temps. Il s'agit généralement de classes mappées sur des tables de base de données à l'aide d'un ORM (comme Nette Database Explorer ou Doctrine). Les entités peuvent contenir des règles métier concernant leurs données et une logique de validation.

// Entité mappée sur la table de base de données orders
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,
		]);
	}
}

Objets Valeur (Value Objects): objets immuables représentant des valeurs sans identité propre – par exemple, un montant monétaire ou une adresse e-mail. Deux instances d'un objet valeur avec les mêmes valeurs sont considérées comme identiques.

Code d'infrastructure

Le dossier Core/ (ou aussi Infrastructure/) abrite la base technique de l'application. Le code d'infrastructure comprend généralement :

app/Core/
├── Router/               ← routage et gestion des URL
│   └── RouterFactory.php
├── Security/             ← authentification et autorisation
│   ├── Authenticator.php
│   └── Authorizator.php
├── Logging/              ← journalisation et surveillance
│   ├── SentryLogger.php
│   └── FileLogger.php
├── Cache/                ← couche de mise en cache
│   └── FullPageCache.php
└── Integration/          ← intégration avec les services ext.
	├── Slack/
	└── Stripe/

Pour les petits projets, une structure plate suffit bien sûr :

Core/
├── RouterFactory.php
├── Authenticator.php
└── QueueMailer.php

Il s'agit de code qui :

  • Gère l'infrastructure technique (routage, journalisation, mise en cache)
  • Intègre des services externes (Sentry, Elasticsearch, Redis)
  • Fournit des services de base pour toute l'application (mail, base de données)
  • Est généralement indépendant du domaine spécifique – le cache ou le logger fonctionne de la même manière pour une boutique en ligne ou un blog.

Vous hésitez si une certaine classe appartient ici ou au modèle ? La différence clé est que le code dans Core/ :

  • Ne sait rien du domaine (produits, commandes, articles)
  • Est généralement portable vers un autre projet
  • Gère “comment ça marche” (comment envoyer un mail), pas “ce que ça fait” (quel mail envoyer)

Exemple pour une meilleure compréhension :

  • App\Core\MailerFactory – crée des instances de la classe pour l'envoi d'e-mails, gère les paramètres SMTP
  • App\Model\OrderMailer – utilise MailerFactory pour envoyer des e-mails concernant les commandes, connaît leurs templates et sait quand ils doivent être envoyés

Scripts de commande

Les applications ont souvent besoin d'effectuer des activités en dehors des requêtes HTTP normales – qu'il s'agisse de traitement de données en arrière-plan, de maintenance ou de tâches périodiques. Pour l'exécution, des scripts simples dans le répertoire bin/ sont utilisés, la logique d'implémentation elle-même est placée dans app/Tasks/ (ou app/Commands/).

Exemple :

app/Tasks/
├── Maintenance/               ← scripts de maintenance
│   ├── CleanupCommand.php     ← suppression des anciennes données
│   └── DbOptimizeCommand.php  ← optimisation de la base de données
├── Integration/               ← intégration avec des systèmes externes
│   ├── ImportProducts.php     ← importation depuis le système fournisseur
│   └── SyncOrders.php         ← synchronisation des commandes
└── Scheduled/                 ← tâches régulières
	├── NewsletterCommand.php  ← envoi de newsletters
	└── ReminderCommand.php    ← notifications aux clients

Qu'est-ce qui appartient au modèle et qu'est-ce qui appartient aux scripts de commande ? Par exemple, la logique pour envoyer un seul e-mail fait partie du modèle, l'envoi en masse de milliers d'e-mails appartient déjà à Tasks/.

Les tâches sont généralement lancées depuis la ligne de commande ou via cron. Elles peuvent également être lancées via une requête HTTP, mais il faut penser à la sécurité. Le presenter qui lance la tâche doit être sécurisé, par exemple uniquement pour les utilisateurs connectés ou avec un jeton fort et un accès depuis des adresses IP autorisées. Pour les tâches longues, il est nécessaire d'augmenter la limite de temps du script et d'utiliser session_write_close() pour ne pas verrouiller la session.

Autres répertoires possibles

En plus des répertoires de base mentionnés, vous pouvez ajouter d'autres dossiers spécialisés en fonction des besoins du projet. Jetons un coup d'œil aux plus courants et à leur utilisation :

app/
├── Api/              ← logique pour l'API indépendante de la couche de présentation
├── Database/         ← scripts de migration et seeders pour les données de test
├── Components/       ← composants visuels partagés dans toute l'application
├── Event/            ← utile si vous utilisez une architecture événementielle
├── Mail/             ← templates d'e-mail et logique associée
└── Utils/            ← classes utilitaires

Pour les composants visuels partagés utilisés dans les presenters à travers l'application, le dossier app/Components ou app/Controls peut être utilisé :

app/Components/
├── Form/                 ← composants de formulaire partagés
│   ├── SignInForm.php
│   └── UserForm.php
├── Grid/                 ← composants pour l'affichage de données
│   └── DataGrid.php
└── Navigation/           ← éléments de navigation
	├── Breadcrumbs.php
	└── Menu.php

C'est ici que se trouvent les composants qui ont une logique plus complexe. Si vous souhaitez partager des composants entre plusieurs projets, il est conseillé de les extraire dans un paquet composer distinct.

Dans le répertoire app/Mail, vous pouvez placer la gestion de la communication par e-mail :

app/Mail/
├── templates/            ← templates d'e-mail
│   ├── order-confirmation.latte
│   └── welcome.latte
└── OrderMailer.php

Mapping des presenters

Le mapping définit les règles pour dériver le nom de la classe à partir du nom du presenter. Nous les spécifions dans la configuration sous la clé application › mapping.

Sur cette page, nous avons montré que nous plaçons les presenters dans le dossier app/Presentation (ou app/UI). Nous devons communiquer cette convention à Nette dans le fichier de configuration. Une seule ligne suffit :

application:
	mapping: App\Presentation\*\**Presenter

Comment fonctionne le mapping ? Pour mieux comprendre, imaginons d'abord une application sans modules. Nous voulons que les classes de presenter appartiennent au namespace App\Presentation, afin que le presenter Home soit mappé sur la classe App\Presentation\HomePresenter. Ce que nous obtenons avec cette configuration :

application:
	mapping: App\Presentation\*Presenter

Le mapping fonctionne de telle sorte que le nom du presenter Home remplace l'astérisque dans le masque App\Presentation\*Presenter, ce qui donne le nom de classe résultant App\Presentation\HomePresenter. Simple !

Mais comme vous pouvez le voir dans les exemples de ce chapitre et d'autres, nous plaçons les classes de presenter dans des sous-répertoires éponymes, par exemple le presenter Home est mappé sur la classe App\Presentation\Home\HomePresenter. Nous obtenons cela en doublant les deux-points (nécessite Nette Application 3.2) :

application:
	mapping: App\Presentation\**Presenter

Passons maintenant au mapping des presenters dans les modules. Pour chaque module, nous pouvons définir un mapping spécifique :

application:
	mapping:
		Front: App\Presentation\Front\**Presenter
		Admin: App\Presentation\Admin\**Presenter
		Api: App\Api\*Presenter

Selon cette configuration, le presenter Front:Home est mappé sur la classe App\Presentation\Front\Home\HomePresenter, tandis que le presenter Api:OAuth est mappé sur la classe App\Api\OAuthPresenter.

Comme les modules Front et Admin ont un mode de mapping similaire et qu'il y aura probablement plus de modules de ce type, il est possible de créer une règle générale qui les remplacera. Une nouvelle astérisque pour le module est ainsi ajoutée au masque de classe :

application:
	mapping:
		*: App\Presentation\*\**Presenter
		Api: App\Api\*Presenter

Cela fonctionne également pour les structures de répertoires plus profondément imbriquées, comme par exemple le presenter Admin:User:Edit, le segment avec l'astérisque se répète pour chaque niveau et le résultat est la classe App\Presentation\Admin\User\Edit\EditPresenter.

Une notation alternative consiste à utiliser un tableau composé de trois segments au lieu d'une chaîne. Cette notation est équivalente à la précédente :

application:
	mapping:
		*: [App\Presentation, *, **Presenter]
		Api: [App\Api, '', *Presenter]
version: 4.0