Structure des répertoires de l'application

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

  • comment structurer logiquement l'application en répertoires
  • comment concevoir la structure pour qu'elle s'adapte bien à 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'insiste pas sur une structure spécifique. Il est conçu pour s'adapter facilement à tous les besoins et à toutes les préférences.

Structure de base du projet

Bien que Nette Framework ne dicte pas de structure de répertoire fixe, il existe un arrangement par défaut éprouvé sous la forme d'un projet Web :

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

Vous pouvez librement modifier cette structure en fonction de vos besoins – renommer ou déplacer des dossiers. Il vous suffit alors d'ajuster les chemins relatifs des répertoires dans Bootstrap.php et éventuellement composer.json. Rien d'autre n'est nécessaire, pas de reconfiguration complexe, pas de changements constants. 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 devez être en mesure de vous orienter rapidement. Imaginez que vous cliquez sur le répertoire app/Model/ et que vous voyez cette structure :

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

Vous apprendrez seulement que le projet utilise certains services, référentiels et entités. Vous n'apprendrez rien sur l'objectif réel de l'application.

Examinons une approche différente – l'organisation par domaines :

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

C'est différent – au premier coup d'œil, il est clair qu'il s'agit d'un site de commerce électronique. Les noms des répertoires eux-mêmes révèlent ce que l'application peut faire – elle fonctionne avec des paiements, des commandes et des produits.

La première approche (organisation par type de classe) pose plusieurs problèmes dans la pratique : 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 allons l'organiser par domaines.

Espaces de noms

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

Singulier et pluriel dans les noms

Remarquez que nous utilisons le singulier pour les principaux répertoires d'applications : app, config, log, temp, www. Il en va de même à l'intérieur de l'application : Model, Core, Presentation. Cela s'explique par le fait que chacun représente un concept unifié.

De même, app/Model/Product représente tout ce qui concerne les produits. Nous ne l'appelons pas Products parce qu'il ne s'agit pas d'un dossier rempli de produits (qui contiendrait des fichiers comme iphone.php, samsung.php). Il s'agit d'un espace de noms contenant des classes permettant de travailler avec des produits – ProductRepository.php, ProductService.php.

Le dossier app/Tasks est pluriel parce qu'il contient un ensemble de scripts exécutables autonomes – CleanupTask.php, ImportTask.php. Chacun d'entre eux est une unité indépendante.

Par souci de cohérence, nous recommandons d'utiliser :

  • le singulier pour les espaces de noms représentant une unité fonctionnelle (même si l'on travaille avec plusieurs entités)
  • Pluriel pour les collections d'unités indépendantes
  • 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 (ce qu'on appelle le document-root). Vous rencontrerez souvent le nom public/ au lieu de www/ – c'est juste une question de convention et n'affecte pas la fonctionnalité. Le répertoire contient

  • Point d'entrée de l' application index.php
  • le fichier .htaccess avec les règles mod_rewrite (pour Apache)
  • Fichiers statiques (CSS, JavaScript, images)
  • Fichiers téléchargés

Pour assurer la sécurité de l'application, il est essentiel que la racine du document soit correctement configurée.

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

Répertoire des applications app/

Il s'agit du répertoire principal contenant le code de l'application. Structure de base :

app/
├── Core/               ← questions d'infrastructure
├── Model/              ← logique d'entreprise
├── Presentation/       ← présentateurs et modèles
├── Tasks/              ← scripts de commande
└── Bootstrap.php       ← classe d'amorçage d'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 les différents sous-répertoires en détail.

Présentateurs et modèles

La partie présentation de l'application se trouve dans le répertoire app/Presentation. Une alternative est le répertoire court app/UI. C'est l'endroit où se trouvent tous les présentateurs, leurs modèles et toutes les classes d'aide.

Nous organisons cette couche par domaines. Dans un projet complexe qui combine le commerce électronique, le blog et l'API, la structure ressemblerait à ceci :

app/Presentation/
├── Shop/              ← e-commerce frontend
│   ├── Product/
│   ├── Cart/
│   └── Order/
├── Blog/              ← blog
│   ├── Home/
│   └── Post/
├── Admin/             ← administration
│   ├── Dashboard/
│   └── Products/
└── Api/               ← points d'extrémité API
	└── V1/

À l'inverse, pour un simple blog, nous utiliserions la structure suivante :

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

Les dossiers tels que Home/ ou Dashboard/ contiennent les présentateurs et les modèles. Les dossiers tels que Front/, Admin/ ou Api/ sont appelés modules. Techniquement, il s'agit de répertoires ordinaires qui servent à l'organisation logique de l'application.

Chaque dossier contenant un présentateur contient un présentateur de même nom et ses modèles. Par exemple, le dossier Dashboard/ contient :

Dashboard/
├── DashboardPresenter.php     ← présentateur
└── default.latte              ← modèle

Cette structure de répertoire se reflète dans les espaces de noms des classes. Par exemple, DashboardPresenter se trouve dans l'espace de noms App\Presentation\Admin\Dashboard (voir le mappage des présentateurs) :

namespace App\Presentation\Admin\Dashboard;

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

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

Développement d'une structure flexible

L'un des grands avantages de cette structure est qu'elle s'adapte élégamment aux besoins croissants des projets. Prenons l'exemple de la partie qui génère des flux XML. Au départ, nous avons un simple formulaire :

Export/
├── ExportPresenter.php   ← un seul présentateur pour toutes les exportations
├── sitemap.latte         ← modèle de plan du site
└── feed.latte            ← modèle pour flux RSS

Au fil du 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
	├── amazon.latte         ← flux pour Amazon
	└── ebay.latte           ← flux pour eBay

Cette transformation se fait en douceur : 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:amazon). Grâce à cela, nous pouvons progressivement étendre la structure en fonction des besoins, le niveau d'imbrication n'étant en aucun cas limité.

Par exemple, si dans l'administration vous avez de nombreux présentateurs liés à la gestion des commandes, tels que OrderDetail, OrderEdit, OrderDispatch etc., vous pouvez créer un module (dossier) Order pour une meilleure organisation, qui contiendra (des dossiers pour) les présentateurs Detail, Edit, Dispatch et d'autres.

Emplacement du modèle

Dans les exemples précédents, nous avons vu que les modèles sont situés directement dans le dossier du présentateur :

Dashboard/
├── DashboardPresenter.php     ← présentateur
├── DashboardTemplate.php      ← classe de modèle optionnelle
└── default.latte              ← modèle

Cet emplacement s'avère être le plus pratique dans la pratique – vous avez tous les fichiers connexes à portée de main.

Vous pouvez également placer les modèles dans un sous-dossier templates/. Nette prend en charge les deux variantes. Vous pouvez même placer les modèles complètement en dehors du dossier Presentation/. Tout ce qui concerne les options d'emplacement des modèles se trouve dans le chapitre Consultation des modèles.

Classes d'aide et composants

Les présentateurs et les modèles sont souvent accompagnés d'autres fichiers d'aide. Nous les plaçons logiquement en fonction de leur portée :

1. Directement avec le présentateur dans le cas de composants spécifiques pour le présentateur donné :

Product/
├── ProductPresenter.php
├── ProductGrid.php        ← composant pour la liste des produits
└── FilterForm.php         ← formulaire de filtrage

2. Pour le module – nous recommandons d'utiliser le dossier Accessory, qui est placé proprement au début de l'alphabet :

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

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

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

Vous pouvez également placer des classes d'aide 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 conventions de l'équipe.

Modèle – Cœur de l'application

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

app/Model/
├── Payment/                   ← tout sur les paiements
│   ├── PaymentFacade.php      ← point d'entrée principal
│   ├── PaymentRepository.php
│   ├── Payment.php            ← entité
├── Order/                     ← tout sur les commandes
│   ├── OrderFacade.php
│   ├── OrderRepository.php
│   ├── Order.php
└── Shipping/                  ← tout sur l'expédition

Dans le modèle, vous rencontrez typiquement ces types de classes :

Facades : elles représentent le principal point d'entrée dans un domaine spécifique de l'application. Elles agissent comme un orchestrateur qui coordonne la coopération entre différents services pour mettre en œuvre des cas d'utilisation complets (comme “créer une commande” ou “traiter un paiement”). Sous sa couche d'orchestration, la façade cache les détails de la mise en œuvre au reste de l'application, fournissant ainsi une interface propre pour travailler avec le domaine donné.

class OrderFacade
{
	public function createOrder(Cart $cart): Order
	{
		// validation
		// création de commandes
		// envoi de courriels
		// écriture dans les statistiques
	}
}

Services : ils se concentrent sur des opérations commerciales spécifiques au sein d'un domaine. Contrairement aux façades qui orchestrent des cas d'utilisation entiers, un service met en œuvre une logique commerciale spécifique (comme le calcul des prix ou le traitement des paiements). Les services sont généralement 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
	}
}

Les référentiels : gèrent toutes les communications avec le stockage des données, généralement une base de données. Leur tâche consiste à charger et à enregistrer des entités et à mettre en œuvre des méthodes de recherche. Un référentiel protège le reste de l'application des détails de la mise en œuvre 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 commerciaux de l'application, qui ont leur identité et changent au fil du 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 de gestion concernant leurs données et leur logique de validation.

// Entité associée à la table de la base de données "commandes".
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 : objets immuables représentant des valeurs sans identité propre – par exemple, un montant d'argent ou une adresse électronique. Deux instances d'un objet valeur ayant les mêmes valeurs sont considérées comme identiques.

Code d'infrastructure

Le dossier Core/ (ou également Infrastructure/) contient 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 des services externes
	├── Slack/
	└── Stripe/

Pour les petits projets, une structure plate est naturellement suffisante :

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

C'est du 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 l'ensemble de l'application (courrier, base de données)
  • est en grande partie indépendant du domaine spécifique – le cache ou le logger fonctionne de la même manière pour un commerce électronique ou un blog.

Vous vous demandez si une certaine classe a sa place ici ou dans le modèle ? La différence essentielle est que le code dans Core/:

  • ne connaît rien du domaine (produits, commandes, articles)
  • peut généralement être transféré vers un autre projet
  • Résout “comment ça marche” (comment envoyer du courrier), et non “ce que ça fait” (quel courrier envoyer).

Exemple pour une meilleure compréhension :

  • App\Core\MailerFactory – crée des instances de la classe d'envoi de courrier électronique, gère les paramètres SMTP
  • App\Model\OrderMailer – utilise MailerFactory pour envoyer des courriels sur les commandes, connaît leurs modèles et le moment où ils doivent être envoyés.

Scripts de commande

Les applications ont souvent besoin d'effectuer des tâches 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. Les scripts simples du répertoire bin/ sont utilisés pour l'exécution, tandis que la logique d'implémentation réelle est placée dans app/Tasks/ (ou app/Commands/).

Exemple :

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

Qu'est-ce qui relève du modèle et qu'est-ce qui relève des scripts de commande ? Par exemple, la logique d'envoi d'un courriel fait partie du modèle, tandis que l'envoi en masse de milliers de courriels relève de Tasks/.

Les tâches sont généralement exécutées à partir de la ligne de commande ou via cron. Elles peuvent également être exécutées via une requête HTTP, mais la sécurité doit être prise en compte. Le présentateur qui exécute la tâche doit être sécurisé, par exemple uniquement pour les utilisateurs connectés ou avec un jeton fort et un accès à partir d'adresses IP autorisées. Pour les tâches de longue durée, il est nécessaire d'augmenter la limite de temps du script et d'utiliser session_write_close() pour éviter de 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. Examinons les plus courants et leur utilisation :

app/
├── Api/              ← logique de l'API indépendante de la couche de présentation
├── Database/         ← des scripts de migration et des semoirs pour les données de test
├── Components/       ← composants visuels partagés dans l'ensemble de l'application
├── Event/            ← utile en cas d'utilisation d'une architecture pilotée par les événements
├── Mail/             ← modèles de courrier électronique et logique connexe
└── Utils/            ← classes d'aide

Pour les composants visuels partagés utilisés dans les présentateurs de l'application, vous pouvez utiliser le dossier app/Components ou app/Controls:

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

C'est dans ce dossier que se trouvent les composants dont la logique est plus complexe. Si vous souhaitez partager des composants entre plusieurs projets, il est préférable de les séparer dans un package de composition autonome.

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

app/Mail/
├── templates/            ← modèles de courrier électronique
│   ├── order-confirmation.latte
│   └── welcome.latte
└── OrderMailer.php

Cartographie des présentateurs

Le mappage définit des règles pour dériver les noms de classes des noms de présentateurs. Nous les spécifions dans la configuration sous la clé application › mapping.

Sur cette page, nous avons montré que nous plaçons les présentateurs dans le dossier app/Presentation (ou app/UI). Nous devons informer Nette de cette convention dans le fichier de configuration. Une 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 présentateurs relèvent de l'espace de noms App\Presentation, de sorte que le présentateur Home soit associé à la classe App\Presentation\HomePresenter. Cette configuration permet d'atteindre cet objectif :

application:
	mapping: App\Presentation\*Presenter

Le mappage s'effectue en remplaçant l'astérisque du masque App\Presentation\*Presenter par le nom du présentateur Home, ce qui donne le nom de classe final App\Presentation\HomePresenter. C'est simple !

Cependant, comme vous le verrez dans les exemples de ce chapitre et d'autres, nous plaçons les classes de présentateurs dans des sous-répertoires éponymes, par exemple le présentateur Home correspond à la classe App\Presentation\Home\HomePresenter. Pour ce faire, nous doublons les deux points (Nette Application 3.2 nécessaire) :

application:
	mapping: App\Presentation\**Presenter

Nous allons maintenant passer au mappage des présentateurs dans les modules. Nous pouvons définir un mappage spécifique pour chaque module :

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

Selon cette configuration, le présentateur Front:Home correspond à la classe App\Presentation\Front\Home\HomePresenter, tandis que le présentateur Api:OAuth correspond à la classe App\Api\OAuthPresenter.

Étant donné que les modules Front et Admin ont une méthode de mappage similaire et qu'il y aura probablement d'autres modules de ce type, il est possible de créer une règle générale qui les remplacera. Un nouvel astérisque pour le module sera ajouté au masque de classe :

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

Cela fonctionne également pour les structures de répertoires imbriqués plus profonds, comme le présentateur Admin:User:Edit, où le segment avec astérisque se répète pour chaque niveau et aboutit à la classe App\Presentation\Admin\User\Edit\EditPresenter.

Une autre notation 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