Frameworks Backend
Un seul problème, résolu trois fois : ce qui se ressemble est le concept, ce qui diffère est la convention.
- Un problème, trois réponses
- Active record contre data mapper
- Échappement par défaut
- Choisir sans vainqueur
À la fin de ce cours, vous saurez
- Dire ce qu’un framework apporte qu’un projet PHP nu n’a pas — et ce qu’il coûte.
- Lire une route, un contrôleur et un gabarit dans les trois frameworks.
- Distinguer active record et data mapper, et dire lequel chaque framework emploie.
- Justifier un choix de framework par le contexte, jamais par la mode.
- Retrouver dans ce site les pièces d’une application CodeIgniter.
Avant de commencer
- POO en PHP — indispensable : les trois reposent sur les classes et les namespaces
- Composer — les trois s’installent et se chargent par lui
- MySQL et PHP — ce qu’un ORM fait à votre place
Ce que ce cours ne couvre pas
Trois frameworks, c'est trois mondes. Cette page compare ce qui se compare — routage, contrôleur, ORM, gabarits — sur un même problème. Le reste a ses propres cours, ou les aura.
| Hors périmètre | Pourquoi |
|---|---|
| Déploiement, Docker, intégration continue | c’est du système, et ce n’est pas propre aux frameworks — Linux |
| Files d’attente, tâches planifiées, événements | chacun mérite son cours, dans un seul framework à la fois |
| Les tests automatisés | un sujet à part entière, commun aux trois |
| L’outillage front de chaque framework (Encore, Vite, npm) | ce sont trois chaînes de compilation, pas trois frameworks |
| Les écosystèmes de bundles et de packages | ils changent plus vite qu’un cours ne se met à jour |
| Les API REST | elles ont leur propre module au programme |
Pourquoi un framework
Vous savez écrire du PHP. Vous savez interroger une base, traiter un formulaire, échapper une sortie. Un framework ne vous apprend rien de tout cela : il vous évite de le réécrire, et surtout de l’oublier.
Ce qu’il apporte
Un point d’entrée UNIQUE. Sans framework, chaque fichier .php est une porte : chacune doit vérifier la session, les droits, l’encodage. Avec, il y a une porte et un routeur derrière.
Des conventions PARTAGÉES. Un développeur qui arrive sait où sont les contrôleurs, parce que c’est la même réponse dans tous les projets du même framework.
Des défenses par défaut : échappement automatique des gabarits, jeton CSRF, requêtes préparées. Ce que le cours des vulnérabilités enseigne à la main, un framework le fait par construction — tant qu’on ne le contourne pas.
Un outillage : console, migrations, générateurs, journalisation.
Ce qu’il coûte
Une dépendance, une courbe d’apprentissage, et des fichiers. Beaucoup de fichiers. Mesurés le 16 août 2026, pour un projet réduit à la même chose dans les trois — une route, un contrôleur, un ORM, un gabarit :
Sortie
ci4 27M 33 paquets 2767 fichiers PHP
symfony 28M 54 paquets 3545 fichiers PHP
laravel 86M 109 paquets 8023 fichiers PHP
Ces chiffres ne classent personne. Un vendor/ volumineux n’est pas un défaut : il n’est jamais envoyé au navigateur, et la moitié n’est chargée qu’à la demande. Ils disent seulement ce que chaque projet embarque d’avance.
Les trois, et leur caractère
Ils font le même métier et ne se ressemblent pas. Versions relevées le 16 août 2026, à la source — chacune installée pour cette page.
bash
php bin/console --version # Symfony
php artisan --version # Laravel
php spark --version # CodeIgniter
Sortie
Symfony v8.1.4 (env: dev, debug: true)
Laravel Framework 13.25.0
CodeIgniter v4.7.4
Symfony
Une collection de composants indépendants, et un squelette qui n’impose presque rien. On ajoute ce dont on a besoin — l’ORM, les gabarits — et rien d’autre. C’est le plus explicite des trois : tout se configure, donc tout se lit. Beaucoup d’autres projets s’appuient sur ses composants, Laravel compris.
Laravel
Le plus fourni. L’ORM, les gabarits, l’authentification, les files d’attente, les tests : tout est là au premier jour, avec une syntaxe qui cherche l’agrément. C’est aussi le plus opiniâtre — il propose une façon de faire, et s’en écarter demande un effort.
CodeIgniter 4
Le plus léger et le plus proche du PHP nu. Peu de magie, peu de conventions imposées, une prise en main courte. C’est le framework de ce site — et la section suivante en montre le code.
Aucun des trois n’est « le meilleur ». Ils répondent à des contraintes différentes : la taille de l’équipe, la durée de vie du projet, ce que vous savez déjà. La dernière section propose une grille, pas un classement.
Ce site est une application CodeIgniter
La page que vous lisez est servie par un framework. Voici ses pièces, telles qu’elles sont sur le disque — pas un exemple, le code réel.
La route
php
<?php
// app/Config/Routes.php
$routes->get('/composer', 'Php\ComposerCours::index');
$routes->get('/wordpress', 'Cms\WordpressCours::index');
$routes->get('/ci4-gestion-contenu', 'Cms\Ci4GestionContenu::index');
Le contrôleur
php
<?php
// app/Controllers/Php/ComposerCours.php
namespace App\Controllers\Php;
class ComposerCours extends \App\Controllers\BaseController
{
public function index()
{
return view('templates/php/composer');
}
}
Une route nomme une classe et une méthode ; la méthode rend une vue. C’est tout ce qui sépare une adresse d’une page — et c’est le même principe dans les trois frameworks, aux conventions près.
Deux honnêtetés sur cet exemple. D’abord, ce site tourne en CodeIgniter 4.6.1, tandis que le bac à sable de cette page est en 4.7.4 : rien de ce qui est montré ici ne diffère entre les deux. Ensuite, ce site N’A PAS DE BASE DE DONNÉES — tout son contenu vit dans app/Config/Courses/. La partie ORM ne peut donc rien lui emprunter, et se démontre au bac à sable.
Le routage
Une même question — « quelle classe répond à /articles ? » — et trois réponses de forme différente.
Symfony : l’attribut, sur la méthode
php
<?php
// src/Controller/ArticleController.php
#[Route('/articles', name: 'article_index')]
public function index(ArticleRepository $articles): Response
{
return $this->render('article/index.html.twig', [
'articles' => $articles->findPublished(),
]);
}
La route vit AVEC le code qu’elle appelle. Rien à tenir à jour ailleurs, et rien à ouvrir pour savoir quelle adresse mène ici — mais aucune vue d’ensemble non plus, sauf à demander la liste à la console.
Laravel : le fichier de routes
php
<?php
// routes/web.php
use App\Http\Controllers\ArticleController;
Route::get('/articles', [ArticleController::class, 'index']);
CodeIgniter : le fichier de routes, lui aussi
php
<?php
// app/Config/Routes.php
$routes->get('/articles', 'Articles::index');
Laravel et CodeIgniter rassemblent leurs routes en un fichier : on lit tout le plan du site d’un coup d’œil, au prix d’un aller-retour quand on travaille sur une action. Symfony les disperse : l’inverse, exactement. Aucune des deux façons n’est meilleure — elles se trompent d’endroit différemment.
Le contrôleur, et ses dépendances
C’est ici que la différence de philosophie se voit le mieux : comment le contrôleur obtient-il ce dont il a besoin ?
Symfony : on le lui donne
php
<?php
public function index(ArticleRepository $articles): Response
{
return $this->render('article/index.html.twig', [
'articles' => $articles->findPublished(),
]);
}
Le dépôt arrive en paramètre. Le contrôleur ne sait pas le construire, et n’a pas à le savoir : le conteneur de services s’en charge, en lisant le TYPE déclaré. C’est l’injection de dépendances, et c’est ce qui rend la classe testable sans base de données.
Laravel : il va le chercher
php
<?php
public function index()
{
return view('articles.index', [
'articles' => Article::published()->get(),
]);
}
Article::published() ne ressemble pas à une méthode statique par hasard : ce n’en est pas une. Laravel intercepte l’appel et le relaie à une instance. C’est court à écrire, et c’est un pas de plus entre ce qu’on lit et ce qui s’exécute.
CodeIgniter : il le construit
php
<?php
public function index()
{
return view('articles/index', [
'articles' => (new ArticleModel())->getPublishedArticles(),
]);
}
Le plus simple à lire : on voit exactement ce qui se passe. Et le plus difficile à tester isolément, puisque le contrôleur décide lui-même de ce qu’il utilise.
L’ORM : deux écoles
Un ORM fait correspondre des lignes de base à des objets PHP. Il y a deux façons de s’y prendre, et les trois frameworks se répartissent entre elles.
Active record : l’objet SAIT s’enregistrer
L’objet porte à la fois les données et l’accès à la base. C’est l’école de Laravel (Eloquent) et de CodeIgniter.
php
<?php
// Laravel — app/Models/Article.php
class Article extends Model
{
protected $fillable = ['title', 'slug', 'content', 'status', 'published_at'];
public function scopePublished($query)
{
return $query->where('status', 'published')
->orderByDesc('published_at');
}
}
php
<?php
// CodeIgniter — app/Models/ArticleModel.php
class ArticleModel extends Model
{
protected $table = 'articles';
protected $allowedFields = ['title', 'slug', 'content', 'status', 'published_at'];
public function getPublishedArticles()
{
return $this->where('status', 'published')
->orderBy('published_at', 'DESC')
->findAll();
}
}
Data mapper : un objet, et quelqu’un pour l’enregistrer
L’entité ne connaît pas la base. Une autre classe — le dépôt — sait l’aller chercher, et un gestionnaire sait l’écrire. C’est l’école de Doctrine, l’ORM de Symfony.
php
<?php
// src/Entity/Article.php — l'entité ne parle JAMAIS à la base
#[ORM\Entity(repositoryClass: ArticleRepository::class)]
class Article
{
#[ORM\Column(length: 255)]
private string $title;
public function getTitle(): string { return $this->title; }
public function setTitle(string $t): static { $this->title = $t; return $this; }
}
php
<?php
// src/Repository/ArticleRepository.php — lui seul interroge
public function findPublished(): array
{
return $this->createQueryBuilder('a')
->andWhere('a.status = :statut')
->setParameter('statut', 'published')
->orderBy('a.publishedAt', 'DESC')
->getQuery()
->getResult();
}
Active record est plus court à écrire et plus rapide à apprendre. Data mapper sépare le métier du stockage : l’entité reste utilisable sans base, donc testable sans base. Le second coûte plus de fichiers ; il les rend au moment où le projet vieillit.
La même sortie, dans les trois
Voici la preuve que ces trois écritures font bien la même chose. Le jeu d’essai est identique — trois articles publiés — et la commande demande les articles publiés, du plus récent au plus ancien.
bash
php bin/console app:peupler # Symfony
php artisan tinker # Laravel
php spark app:peupler # CodeIgniter
Sortie
3 article(s)
- Premier article (premier-article)
- Deuxième article (deuxieme-article)
- Troisième article (troisieme-article)
Trois fois la même sortie, à la ligne près. C’est ce que « faire le même métier » veut dire.
Les gabarits
La boucle d’affichage, dans les trois moteurs. Chacune produit exactement les mêmes lignes.
Twig — Symfony
html
{% for article in articles %}
- {{ article.title }} ({{ article.slug }})
{% endfor %}
Blade — Laravel
html
@foreach ($articles as $article)
- {{ $article->title }} ({{ $article->slug }})
@endforeach
PHP — CodeIgniter
php
<?php foreach ($articles as $article): ?>
- <?= esc($article['title']) ?> (<?= esc($article['slug']) ?>)
<?php endforeach ?>
La différence la plus importante n’est pas la syntaxe : c’est L’ÉCHAPPEMENT. Twig et Blade échappent par défaut — {{ }} est sûr, et il faut demander explicitement le contraire. CodeIgniter emploie le PHP nu : <?= $x ?> n’échappe RIEN, et c’est à vous d’écrire esc(). Oublier ce mot, c’est ouvrir une faille XSS.
C’est le meilleur argument des moteurs dédiés : ils rendent le geste sûr par défaut, et le geste dangereux visible.
Choisir, sans vainqueur
Aucun de ces trois frameworks n’est meilleur que les autres. Ils répondent à des contraintes, et la bonne question n’est jamais « lequel est le meilleur ? » mais « lequel pour CE projet, avec CETTE équipe ? ».
La grille
Vous apprenez, et vous voulez voir ce qui se passe. CodeIgniter : peu de magie, le chemin de la requête est lisible de bout en bout.
Vous voulez livrer vite, avec l’authentification, les files et les tests déjà en place. Laravel : tout est fourni, et la documentation est immense.
Le projet vivra dix ans et changera de mains. Symfony : tout est explicite, l’entité ne dépend pas de la base, et la séparation des couches tient à l’usure.
L’équipe connaît déjà l’un des trois. **C’est le critère le plus fort de la liste**, et de loin. Un framework maîtrisé bat un framework supérieur, toujours.
L’hébergement est contraint, la version de PHP est ancienne. Vérifiez AVANT : Symfony 8.1 exige PHP 8.4, Laravel 13 exige 8.3, CodeIgniter 4.7 se contente de 8.1. Relevé le 16 août 2026.
Ce qui ne doit pas décider
- La popularité seule : elle mesure une communauté, pas l’adéquation à votre problème.
- Un banc d’essai de performance : la différence entre les trois est négligeable devant une requête SQL mal écrite.
- La mode : les trois seront là dans cinq ans.
Aller plus loin
Le site propose un cours complet sur CodeIgniter 4 — modèle, migration, contrôleurs, CRUD et gabarits, jusqu’au projet d’un blog. C’est la suite naturelle de cette page.
Trois réponses, une seule question
Un framework maîtrisé bat un framework supérieur. Le reste — routage, contrôleur, ORM, gabarits — se transpose d’un framework à l’autre en quelques heures, une fois les concepts acquis.