Vous gérez un établissement scolaire et vous utilisez Laravel ? Alors la gestion des années académiques est probablement au cœur de votre application. Mais comment passer d'un contrôleur « fourre-tout » à une architecture moderne, maintenable et prête pour la production ?
Dans cet article, on va voir ensemble comment transformer un simple contrôleur CRUD en une machine de guerre bien huilée, en utilisant trois piliers de Laravel 13 : le Pattern Action, les API Resources avec leur auto-discovery, et la Context Facade pour l'audit. Préparez-vous, ça va changer votre façon de coder !
Mots-clés : Laravel 13, gestion année académique, PHP 8 attributes, CloseAcademicYearAction, API Resources Laravel, Context facade.
Pourquoi cette refonte était indispensable
Avant, notre contrôleur AnneeAcademiqueController faisait tout. Il validait, créait, activait, désactivait, et même gérait les transactions. Un vrai couteau suisse. Mais un couteau suisse, c'est pratique jusqu'au jour où vous voulez ajouter une fonctionnalité comme le verrouillage des notes en fin d'année. Là, le bordel commence.
Le constat était simple :
Logique métier mélangée avec la réponse HTTP — impossible de tester l'activation d'une année sans faire une requête HTTP complète.
Aucune traçabilité — quand une année était activée, personne ne savait qui l'avait fait ni quelle année était active avant.
Couplage fort — pour fermer une année (verrouiller les notes, archiver les bilans), il fallait tout réécrire dans le contrôleur.
Bref, c'était le moment de passer à une architecture plus propre. Et Laravel 13 nous tendait la perche avec des fonctionnalités excellentes.
Le Pattern Action : sortez la logique de vos contrôleurs
Qu'est-ce qu'une Action ?
Une Action est une classe PHP dédiée à une seule responsabilité métier. Elle ne sait pas ce qu'est une requête HTTP, une redirection ou un message flash. Elle reçoit des données, fait son travail, et éventuellement retourne un résultat.
Imaginez un chef cuisinier. Le contrôleur, c'est le service en salle qui prend la commande. L'Action, c'est le chef en cuisine qui prépare le plat. Le service n'a pas besoin de savoir comment le plat est cuisiné, et le chef ne s'occupe pas de la présentation en salle.
CloseAcademicYearAction : notre première Action
Voici ce que fait concrètement CloseAcademicYearAction :
class CloseAcademicYearAction
{
public function execute(AnneeAcademique $annee): void
{
// 1. Désactiver l'ancienne année active
// 2. Activer la nouvelle année
// 3. Enregistrer le contexte d'audit (Context::add)
// 4. Logger l'opération
}
}Pourquoi c'est mieux :
✅ Testable unitairement — on peut tester
execute()sans jamais toucher à une route HTTP.✅ Réutilisable — l'activation est appelée depuis
store(),update()ET la route dédiéeactivate(). Plus de duplication.✅ Évolutif — quand il faudra verrouiller les notes, archiver les bilans, ou envoyer une notification, on ajoute une étape dans l'Action. Le contrôleur, lui, ne change pas.
Et les middleware alors ?
Avec Laravel 13, on n'écrit plus les middleware dans le constructeur ou les routes. On utilise des attributs PHP 8 directement sur la classe :
use Illuminate\Routing\Attributes\Controllers\Middleware;
#[Middleware('auth')]
#[Middleware('verified')]
#[Middleware('role:admin,superadmin')]
class AnneeAcademiqueController extends Controller
{
// ...
}Pourquoi c'est génial :
La sécurité est déclarée là où elle s'applique — directement sur le contrôleur.
Plus besoin de chercher dans les routes pour savoir si un contrôleur est protégé.
Les attributs sont lisibles par les outils d'analyse statique (PHPStan, IDE).
API Resources : dites adieu au json_encode sauvage
Le problème
Avant, pour sérialiser une année académique en JSON, on faisait ça :
return response()->json([
'id' => $annee->id,
'libelle' => $annee->libelle,
'is_active' => $annee->is_active,
]);Pas faux. Mais imaginez 10 endpoints, 15 modèles, et des relations qui changent. Vous allez vite vous retrouver avec 40 endroits différents qui sérialisent la même chose. Et si vous ajoutez un champ, il faut le changer partout.
La solution : AnneeAcademiqueResource
On crée une classe dédiée qui dit comment transformer le modèle :
class AnneeAcademiqueResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'libelle' => $this->libelle,
'date_debut' => $this->date_debut?->format('Y-m-d'),
'date_fin' => $this->date_fin?->format('Y-m-d'),
'is_active' => $this->is_active,
'statut' => $this->when($this->is_active, 'Année Actuelle'),
];
}
}Les petits plus qui changent tout :
Méthode | Utilité | Exemple |
|---|---|---|
| N'affiche un champ que si une condition est vraie |
|
| N'affiche un count que s'il a été chargé |
|
| N'affiche une agrégation SQL que si elle a été calculée |
|
Résultat : plus de N+1, plus de données inutiles envoyées au client.
L'auto-discovery : encore plus simple
Avec Laravel 13, on enregistre la Resource directement sur le modèle :
use App\Http\Resources\AnneeAcademiqueResource;
use Illuminate\Database\Eloquent\Attributes\UseResource;
#[UseResource(AnneeAcademiqueResource::class)]
class AnneeAcademique extends Model
{
// ...
}Et maintenant, on peut faire :
// Au lieu de : new AnneeAcademiqueResource($annee)
return $annee->toResource();
// Au lieu de : AnneeAcademiqueResource::collection($annees)
return AnneeAcademique::all()->toResourceCollection();
Pas besoin de se rappeler quel Resource va avec quel modèle. Laravel le découvre tout seul. Magique, non ?
Context Facade : l'audit qui vous veut du bien
Le problème du logging traditionnel
Quand vous loggez une action :
Log::info('Année activée', ['annee_id' => 1]);Ce log vit tout seul. Il ne sait pas dans quel contexte il a été émis — quel administrateur était connecté, quelle était l'année précédente, à quelle heure précise. Et si vous devez reconstituer l'historique des activations, bon courage.
La solution : Context::add()
La Context Facade de Laravel 13 permet d'attacher des métadonnées à TOUS les logs émis pendant une requête :
use Illuminate\Support\Facades\Context;
Context::add('academic_year_id', $annee->id);
Context::add('academic_year', [
'activated_id' => $annee->id,
'previous_active_id' => $previousActive?->id,
'action' => 'close_academic_year',
'admin_id' => auth()->id(),
]);Ce que ça change :
Tous les logs émis après cet appel incluront automatiquement ces données.
Même les jobs dispatchés plus tard récupèrent le contexte. Imaginez un job qui envoie un email de confirmation après l'activation : le log de cet email contiendra
academic_year_idsans que vous ayez à le passer manuellement.La traçabilité devient automatique et exhaustive, sans effort supplémentaire.
Question rhétorique : Combien de fois avez-vous passé 2 heures à chercher "qui a changé quoi et quand" dans vos logs ? Avec Context, cette question trouve une réponse en 2 secondes.
Architecture finale : vue d'ensemble
Voici comment tout s'articule :
[Route] → AnneeAcademiqueController
│
├── Appelle → CloseAcademicYearAction
│ ├── Désactive l'ancienne année
│ ├── Active la nouvelle
│ ├── Context::add('academic_year_id')
│ └── Log::info(...)
│
├── Retou
e → $annee->toResource() (JSON)
│ └── AnneeAcademiqueResource
│ ├── when()
│ ├── whenCounted()
│ └── whenAggregated()
│
└── Middleware (attributs) → #[Middleware('auth')]
#[Middleware('role:admin,superadmin')]Avant (contrôleur monolithique) :
~150 lignes de tout et n'importe quoi
Logique d'activation noyée dans le
store()Aucune traçabilité
Duplication de code
Après (architecture moderne) :
Contrôleur allégé : ~80 lignes, que de l'orchestration
Action dédiée, testable, réutilisable
Resource standardisée, auto-découverte
Contexte d'audit automatique
Bonnes pratiques à retenir
Une Action = une responsabilité. Si votre méthode
execute()fait plus de 15-20 lignes, découpez.Les contrôleurs ne devraient jamais faire de calcul métier. Validez les entrées, appelez une Action, retournez une réponse.
Utilisez
#[UseResource]sur tous vos modèles. Même si vous ne faites que du Blade aujourd'hui, vos futures API vous remercieront.Context::add()dès qu'une opération importante est faite. Le logging basique ne suffit pas pour l'audit.Adoptez les attributs PHP 8 pour les middleware. Plus propre, plus déclaratif, plus maintenable.
❓ FAQ
Q1 : L'Action doit-elle gérer les transactions ?
Ça dépend. Si l'Action fait plusieurs opérations atomiques (désactiver + activer + logger), oui, gérez la transaction dans l'Action. Sinon, laissez le contrôleur gérer. L'important, c'est la cohérence.
Q2 : Les API Resources sont-elles utiles si je n'utilise que Blade ?
Oui, et voici pourquoi : le jour où vous aurez besoin d'une API (et vous en aurez besoin — une SPA, une app mobile, un partenaire externe), vous serez prêt. Et même pour Blade, la Resource vous force à réfléchir à la représentation des données, ce qui améliore la qualité du code.
Q3 : Context ralentit-il l'application ?
Non. Context est conçu pour être léger. Il utilise un stockage local au processus, sans accès disque ni réseau. Les données sont sérialisées automatiquement dans les logs et les jobs sans surcoût significatif.
Q4 : Puis-je utiliser toResource() dans une vue Blade ?
Théoriquement oui, mais ce n'est pas l'usage principal. Les vues Blade ont besoin d'accéder aux propriétés des modèles directement ($annee->libelle). La Resource brille surtout pour le JSON. Pour Blade, gardez le modèle.
Q5 : Que faire si je veux fermer définitivement une année (verrouiller les notes) ?
C'est exactement le scénario pour lequel CloseAcademicYearAction a été conçu. Ajoutez une étape dans la méthode execute() :
$annee->evaluations()->update(['is_locked' => true]);
$annee->bilansCompetences()->update(['is_finalized' => true]);Le contrôleur ne change pas. L'Action s'enrichit. La maintenance reste simple.
En résumé
On a vu comment transformer la gestion des années académiques avec Laravel 13 :
✅ CloseAcademicYearAction extrait la logique métier du contrôleur
✅ #[Middleware] attributs PHP 8 remplacent les appels middleware dans les routes
✅ AnneeAcademiqueResource standardise la sérialisation avec when/whenCounted/whenAggregated
✅ #[UseResource] sur le modèle permet l'auto-discovery par
toResource()✅ Context::add('academic_year_id') assure une traçabilité automatique dans tous les logs
Ce n'est pas juste une refonte technique. C'est un changement de philosophie : chaque couche a un rôle précis, chaque classe fait une seule chose, et le système devient prévisible, testable et évolutif.
Alors, prêt à moderniser votre code ? Ouvrez votre IDE, créez votre première Action, et dites adieu aux contrôleurs spaghetti. Vous verrez, c'est jouissif.
Et surtout : amusez-vous !






