Frameworks Backend

Un seul problème, résolu trois fois : ce qui se ressemble est le concept, ce qui diffère est la convention.

À la fin de ce cours, vous saurez

  1. Dire ce qu’un framework apporte qu’un projet PHP nu n’a pas — et ce qu’il coûte.
  2. Lire une route, un contrôleur et un gabarit dans les trois frameworks.
  3. Distinguer active record et data mapper, et dire lequel chaque framework emploie.
  4. Justifier un choix de framework par le contexte, jamais par la mode.
  5. 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.

Sujets hors périmètre et raison de leur exclusion
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.