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 SMTPApp\Model\OrderMailer
– utiliseMailerFactory
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]