Apprentissage avancé de la Programmation Backend avec Laravel et PHP
Apprentissage avancé de la Programmation Backend avec Laravel et PHP

Création de mini-frameworks pour comprendre les rouages internes

Contexte du cours : Apprentissage avancé de la Programmation Backend avec Laravel et PHP

Introduction : Pourquoi bâtir un mini-framework ?

En tant que développeurs backend, nous utilisons quotidiennement des frameworks puissants comme Laravel. Ces outils nous offrent une productivité incroyable en gérant une multitude de tâches complexes en arrière-plan. Cependant, cette abstraction, si elle est bénéfique pour le développement rapide, peut parfois masquer le comment et le pourquoi des choses.

Cette leçon vous propose de lever le voile sur ces mécanismes. L'objectif n'est pas de réinventer Laravel, mais de construire ensemble un mini-framework rudimentaire. Cette expérience vous permettra de :

  • Comprendre l'invisible : Appréhender les principes fondamentaux (routage, injection de dépendances, cycle de vie d'une requête) qui opèrent sous le capot de Laravel.
  • Devenir un meilleur développeur Laravel : En comprenant les bases, vous saurez mieux déboguer, optimiser et étendre les fonctionnalités de Laravel de manière appropriée.
  • Démystifier les concepts clés : Transformer des notions abstraites en implémentations concrètes.

Préparez-vous à plonger dans l'architecture logicielle et à éclairer les zones d'ombre souvent prises pour acquises !

Qu'est-ce qu'un "framework" et pourquoi en recréer un ?

Un framework est une armature logicielle qui fournit une structure de base pour développer des applications. Il inclut généralement :

  • Des conventions de code.
  • Des outils et bibliothèques pré-construites pour des tâches courantes (routage, accès base de données, gestion des sessions, etc.).
  • Un "flux de contrôle" prédéfini, où le framework appelle votre code (c'est le principe de l'Inversion de Contrôle).

Objectifs de l'exercice :

Construire un mini-framework, même simplifié, est un exercice d'apprentissage puissant :

  • Maîtrise des design patterns : Vous appliquerez des patterns essentiels comme le Front Controller, l'Injection de Dépendances, le Service Locator (indirectement), etc.
  • Résolution de problématiques d'architecture : Comment organiser le code ? Comment rendre les composants réutilisables ? Comment gérer les dépendances ?
  • Flexibilité et adaptabilité : Comprendre comment les frameworks sont conçus pour être extensibles et comment leurs différentes parties s'articulent.
  • Développement plus éclairé : Chaque ligne de code que nous écrirons reflétera une décision de conception présente, sous une forme plus élaborée, dans les grands frameworks.

Les briques fondamentales d'un mini-framework

Pour notre mini-framework, nous allons nous concentrer sur les composants essentiels qui forment le cœur de tout framework web moderne :

  1. Le Front Controller : Le point d'entrée unique de toutes les requêtes web.
  2. L'Autoloading : Un mécanisme pour charger automatiquement les classes PHP sans require manuel.
  3. Le Routage : Le système qui mappe les URL aux actions de votre application.
  4. Les Objets Requête et Réponse : Des abstractions orientées objet pour gérer les données de la requête HTTP entrante et la réponse HTTP sortante.
  5. Le Conteneur d'Inversion de Contrôle (IoC) / Injection de Dépendances (DI) : Un registre central pour gérer les dépendances et l'instanciation des objets.
  6. Le Système de Vues : Une méthode simple pour générer la sortie HTML et séparer la logique de présentation.

Étape 1 : Le Front Controller et l'Autoloading

Chaque application basée sur un framework web suit un principe clé : toutes les requêtes sont dirigées vers un unique fichier PHP. C'est le Front Controller. Ce fichier est responsable d'initialiser le framework, de traiter la requête et de renvoyer la réponse.

Le Front Controller : public/index.php

Créez un répertoire public à la racine de votre projet, et à l'intérieur, le fichier index.php.

// public/index.php

// 1. Charger l'autoloader de Composer (nous allons le configurer ensuite)
require __DIR__ . '/../vendor/autoload.php';

// Le reste de notre application sera initialisé ici
// et la requête sera traitée pour envoyer la réponse.

L'Autoloading avec Composer

Pour éviter des require ou include manuels pour chaque classe, nous allons utiliser Composer, le gestionnaire de dépendances de PHP, pour l'autoloading (chargement automatique des classes). Nous suivrons la norme PSR-4.

À la racine de votre projet, créez un fichier composer.json :

// composer.json
{
    "name": "mon-app/mini-framework",
    "description": "Un mini-framework pour la compréhension des rouages internes",
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    },
    "require": {}
}

Ce fichier indique à Composer que toutes les classes du namespace App\ se trouvent dans le répertoire src/. Maintenant, créez le dossier src/ à la racine de votre projet.

Exécutez composer install dans votre terminal à la racine du projet. Composer va générer le fichier vendor/autoload.php, qui est le fichier que notre public/index.php charge.


Étape 2 : Le Routage

Le système de routage est le cerveau de notre application. Il est responsable de déterminer quelle partie de notre code doit être exécutée en fonction de l'URL (URI) demandée et de la méthode HTTP (GET, POST, etc.).

Création de la classe Router

Créez le fichier src/Router.php. Cette classe stockera les routes et aura une méthode pour les "résoudre".

// src/Router.php
<?php

namespace App;

use App\Request; // Nous allons l'utiliser plus tard

class Router
{
    /**
     * Stocke les routes enregistrées, triées par méthode HTTP.
     * Exemple: ['GET' => ['/' => callable], 'POST' => ['/submit' => callable]]
     * @var array<string, array<string, callable|array>>
     */
    protected array $routes = [];

    /**
     * Enregistre une route pour la méthode GET.
     *
     * @param string $uri L'URI à matcher (ex: '/')
     * @param callable|array $action La fonction ou le tableau [Controleur::class, 'methode'] à exécuter.
     * @return void
     */
    public function get(string $uri, callable|array $action): void
    {
        $this->routes['GET'][$uri] = $action;
    }

    /**
     * Enregistre une route pour la méthode POST.
     *
     * @param string $uri L'URI à matcher.
     * @param callable|array $action La fonction ou le tableau [Controleur::class, 'methode'] à exécuter.
     * @return void
     */
    public function post(string $uri, callable|array $action): void
    {
        $this->routes['POST'][$uri] = $action;
    }

    /**
     * Résout la route actuelle en fonction de la Request.
     *
     * @param Request $request L'objet Request encapsulant la requête HTTP.
     * @return mixed Le résultat de l'action de la route (peut être une string, une Response, etc.)
     * @throws \Exception Si la route n'est pas trouvée ou si l'action est invalide.
     */
    public function resolve(Request $request): mixed
    {
        $uri = $request->uri();
        $method = $request->method();

        // Récupère l'action associée à l'URI et à la méthode HTTP.
        $action = $this->routes[$method][$uri] ?? null;

        if (!$action) {
            // Si aucune action n'est trouvée, lance une exception 404.
            throw new \Exception("Route not found for $method $uri", 404);
        }

        $result = null;
        if (is_callable($action)) {
            // Si l'action est une fonction anonyme, l'appelle en lui passant l'objet Request.
            $result = call_user_func($action, $request);
        } elseif (is_array($action) && count($action) === 2) {
            // Si l'action est un tableau [NomDeClasse::class, 'nomDeMethode'],
            // on suppose que c'est un contrôleur.
            [$controllerClass, $method] = $action;

            if (class_exists($controllerClass) && method_exists($controllerClass, $method)) {
                // Instancie le contrôleur (simple instanciation pour l'instant, DI viendra plus tard).
                $controller = new $controllerClass();
                // Appelle la méthode du contrôleur en lui passant l'objet Request.
                $result = call_user_func_array([$controller, $method], [$request]);
            }
        }

        // Si le résultat est une instance de Response (que nous créerons bientôt),
        // c'est parfait. Sinon, on renvoie le résultat tel quel.
        return $result;
    }
}

Création d'un contrôleur simple

Pour les actions sous forme de tableau [Controller::class, 'method'], nous avons besoin d'un contrôleur. Créez un dossier src/Controllers/ et le fichier src/Controllers/HomeController.php :

// src/Controllers/HomeController.php
<?php

namespace App\Controllers;

use App\Request; // Sera utilisé plus tard
use App\Response; // Sera utilisé plus tard

class HomeController
{
    public function about(Request $request): Response // Nous allons modifier le Router pour passer la Request
    {
        return new Response("À propos de notre mini-framework (via contrôleur).");
    }
}

Utilisation dans public/index.php

Nous allons maintenant connecter le routeur à notre front controller.

// public/index.php (mis à jour)
<?php

require __DIR__ . '/../vendor/autoload.php';

use App\Router;
use App\Request;     // À créer dans l'étape 3
use App\Response;    // À créer dans l'étape 3
use App\Controllers\HomeController;

// --- Étape 3 : Création de la Request (temporaire ici pour le Router) ---
// Pour que le Router puisse fonctionner, nous avons besoin d'un objet Request.
// Nous allons le créer de manière plus propre dans la prochaine étape.
// Pour l'instant, une version simplifiée pour le test:
if (!class_exists(Request::class)) {
    class Request {
        public function uri(): string { return parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); }
        public function method(): string { return $_SERVER['REQUEST_METHOD']; }
    }
}
if (!class_exists(Response::class)) {
    class Response {
        protected string $content;
        protected int $statusCode;
        public function __construct(string $content = '', int $statusCode = 200) { $this->content = $content; $this->statusCode = $statusCode; }
        public function send(): void { http_response_code($this->statusCode); echo $this->content; }
    }
}
// --- Fin de l'étape 3 temporaire ---


$router = new Router();

// Définition des routes
$router->get('/', function(Request $request) { // La Request est passée ici
    return new Response("Bienvenue sur notre mini-framework !");
});

$router->get('/about', [HomeController::class, 'about']); // Le contrôleur reçoit la Request

$router->get('/contact', function(Request $request) {
    return new Response("Contactez-nous ici.");
});

// Créer l'objet Request qui sera passé au routeur
$request = new Request();

try {
    // Résoudre la route et obtenir le résultat (qui devrait être un objet Response ou une string)
    $response = $router->resolve($request);

    // Si le résultat n'est pas déjà un objet Response (ex: si l'action retourne une string),
    // enveloppez-le dans une Response par défaut.
    if (!($response instanceof Response)) {
        $response = new Response((string) $response);
    }
    
    // Envoyer la réponse au navigateur
    $response->send();

} catch (\Exception $e) {
    // Gestion des erreurs (ex: route non trouvée)
    $statusCode = $e->getCode() ?: 500; // Utilise le code d'exception ou 500 par défaut
    (new Response($e->getMessage(), $statusCode))->send();
}

Pour tester, vous pouvez configurer un serveur web local pour pointer vers le dossier public (ex: Apache, Nginx), ou utiliser le serveur web intégré de PHP : php -S localhost:8000 -t public Puis accédez à http://localhost:8000/ ou http://localhost:8000/about.


Étape 3 : Les Objets Requête et Réponse

$_GET, $_POST, $_SERVER, header()... Manipuler directement les variables globales et les fonctions PHP pour l'HTTP est fastidieux et rend le code difficilement testable. Les frameworks encapsulent ces détails dans des objets dédiés : Request et Response.

Classe Request

Créez le fichier src/Request.php. Il encapsulera les données de la requête HTTP.

// src/Request.php
<?php

namespace App;

class Request
{
    protected array $getParams;
    protected array $postParams;
    protected array $serverParams;
    protected array $headers;
    protected string $body; // Pour les requêtes POST/PUT avec un corps JSON/XML

    public function __construct()
    {
        $this->getParams = $_GET;
        $this->postParams = $_POST;
        $this->serverParams = $_SERVER;
        // getallheaders() est une fonction Apache, pas toujours disponible avec PHP-FPM/Nginx
        // Pour une approche plus robuste, il faudrait lire les entêtes depuis $_SERVER.
        $this->headers = function_exists('getallheaders') ? getallheaders() : [];
        $this->body = file_get_contents('php://input'); // Utile pour les API REST (JSON, XML...)
    }

    /**
     * Retourne l'URI de la requête (le chemin après le nom de domaine).
     */
    public function uri(): string
    {
        // Nettoie l'URI pour retirer les paramètres de requête (query string)
        return parse_url($this->serverParams['REQUEST_URI'] ?? '/', PHP_URL_PATH);
    }

    /**
     * Retourne la méthode HTTP de la requête (GET, POST, PUT, DELETE...).
     */
    public function method(): string
    {
        return $this->serverParams['REQUEST_METHOD'] ?? 'GET';
    }

    /**
     * Récupère une valeur d'entrée (GET ou POST).
     *
     * @param string $key La clé de l'entrée.
     * @param mixed $default La valeur par défaut si la clé n'existe pas.
     * @return mixed
     */
    public function input(string $key, mixed $default = null): mixed
    {
        return $this->postParams[$key] ?? $this->getParams[$key] ?? $default;
    }

    /**
     * Retourne toutes les entrées (GET et POST).
     */
    public function all(): array
    {
        return array_merge($this->getParams, $this->postParams);
    }

    /**
     * Retourne le corps brut de la requête (utile pour les API JSON/XML).
     */
    public function body(): string
    {
        return $this->body;
    }

    /**
     * Retourne une valeur d'entête.
     */
    public function header(string $key, mixed $default = null): mixed
    {
        // Les entêtes sont généralement préfixées par 'HTTP_' dans $_SERVER
        // getallheaders() les normalise, mais $_SERVER est plus universel.
        $serverKey = 'HTTP_' . strtoupper(str_replace('-', '_', $key));
        return $this->headers[$key] ?? $this->serverParams[$serverKey] ?? $default;
    }
}

Classe Response

Créez le fichier src/Response.php. Elle encapsulera la réponse HTTP à envoyer.

// src/Response.php
<?php

namespace App;

class Response
{
    protected string $content;
    protected int $statusCode;
    protected array $headers = [];

    public function __construct(string $content = '', int $statusCode = 200, array $headers = [])
    {
        $this->content = $content;
        $this->statusCode = $statusCode;
        $this->headers = $headers;
    }

    public function setContent(string $content): self
    {
        $this->content = $content;
        return $this;
    }

    public function setStatusCode(int $statusCode): self
    {
        $this->statusCode = $statusCode;
        return $this;
    }

    public function addHeader(string $name, string $value): self
    {
        $this->headers[$name] = $value;
        return $this;
    }

    /**
     * Envoie les entêtes HTTP et le contenu de la réponse au navigateur.
     */
    public function send(): void
    {
        http_response_code($this->statusCode); // Définit le code de statut HTTP
        foreach ($this->headers as $name => $value) {
            header("$name: $value"); // Envoie les entêtes
        }
        echo $this->content; // Envoie le contenu
    }

    /**
     * Crée une réponse JSON.
     * @param array $data Les données à encoder en JSON.
     * @param int $statusCode Le code de statut HTTP.
     * @return Response
     */
    public static function json(array $data, int $statusCode = 200): Response
    {
        return new self(json_encode($data), $statusCode, ['Content-Type' => 'application/json']);
    }
}

Adaptation de public/index.php

Maintenant que nos classes Request et Response sont complètes, nous pouvons supprimer les définitions temporaires dans index.php et les utiliser réellement.

// public/index.php (mis à jour avec Request et Response)
<?php

require __DIR__ . '/../vendor/autoload.php';

use App\Router;
use App\Request;
use App\Response;
use App\Controllers\HomeController;

$router = new Router();

// Définition des routes
$router->get('/', function(Request $request) {
    // Utilisation de l'objet Request
    $name = $request->input('name', 'Monde');
    // Retourne un objet Response
    return new Response("Bienvenue, {$name} sur notre mini-framework !");
});

$router->get('/about', [HomeController::class, 'about']);

$router->get('/api/users', function(Request $request) {
    // Exemple d'utilisation de la requête et de la réponse JSON
    $search = $request->input('search');
    $users = ['Alice', 'Bob', 'Charlie'];
    if ($search) {
        $users = array_filter($users, fn($user) => str_contains(strtolower($user), strtolower($search)));
    }
    return Response::json(['message' => 'Liste des utilisateurs', 'data' => $users]);
});

// Créer l'objet Request
$request = new Request();

try {
    // Résoudre la route via le routeur, qui retourne maintenant toujours un objet Response
    $response = $router->resolve($request);
    
    // Envoyer la réponse
    $response->send();

} catch (\Exception $e) {
    // Gestion des erreurs
    $statusCode = $e->getCode() ?: 500;
    (new Response($e->getMessage(), $statusCode))->send();
}

Pour la méthode about dans HomeController, assurez-vous qu'elle retourne bien un Response :

// src/Controllers/HomeController.php (mis à jour)
<?php

namespace App\Controllers;

use App\Request;
use App\Response;

class HomeController
{
    public function about(Request $request): Response
    {
        $frameworkName = $request->input('name', 'Mini-Framework');
        return new Response("À propos de {$frameworkName}. C'est un projet d'apprentissage.");
    }
}

Maintenant, votre application gère les requêtes et réponses de manière plus propre et plus modulaire.


Étape 4 : Le Conteneur d'Inversion de Contrôle (IoC) / Injection de Dépendances (DI)

L'Inversion de Contrôle (IoC) signifie que le framework prend le contrôle de la création et de la gestion des objets, plutôt que votre code ne les instancie directement. Le Conteneur IoC (aussi appelé Conteneur de Services) est l'implémentation de ce principe.

L'Injection de Dépendances (DI) est une forme spécifique d'IoC où les dépendances d'une classe (autres objets dont elle a besoin pour fonctionner) lui sont fournies (injectées) au lieu d'être créées par la classe elle-même.

C'est une notion cruciale dans les frameworks modernes comme Laravel, qui permet :

  • Découplage : Les classes ne créent pas leurs dépendances, elles les reçoivent, ce qui les rend moins dépendantes de l'implémentation concrète de ces dépendances.
  • Testabilité : Il est facile de "simuler" des dépendances lors des tests unitaires (mocking).
  • Flexibilité : On peut changer l'implémentation d'un service à un endroit sans modifier tout le code qui l'utilise.

Principe du Conteneur IoC

Un conteneur IoC est un registre qui sait comment construire (instancier) des objets. Vous "liez" (bindez) des interfaces à des implémentations concrètes ou des noms de classes à des fonctions de création. Quand vous avez besoin d'un objet, vous "demandez" au conteneur de le "fabriquer" (make ou resolve).

Création de la classe Container

Créez le fichier src/Container.php.

// src/Container.php
<?php

namespace App;

class Container
{
    protected array $bindings = [];
    protected array $instances = [];

    /**
     * Lie un "abstract" (généralement un nom de classe ou une interface) à un "concrete" (comment l'instancier).
     * Chaque appel à `make` créera une nouvelle instance.
     *
     * @param string $abstract Le nom de la classe ou de l'interface à lier.
     * @param callable $concrete Une fonction qui retourne une instance de l'objet. Le conteneur lui-même est passé en premier argument.
     * @return void
     */
    public function bind(string $abstract, callable $concrete): void
    {
        $this->bindings[$abstract] = $concrete;
        // S'assure que ce n'est pas un singleton si cela avait été enregistré comme tel auparavant
        unset($this->instances[$abstract]);
    }

    /**
     * Lie un "abstract" à un "concrete" et s'assure qu'une seule instance est créée (singleton).
     *
     * @param string $abstract Le nom de la classe ou de l'interface à lier.
     * @param callable $concrete Une fonction qui retourne une instance de l'objet.
     * @return void
     */
    public function singleton(string $abstract, callable $concrete): void
    {
        $this->bindings[$abstract] = $concrete;
        $this->instances[$abstract] = null; // Marque que c'est un singleton, mais pas encore instancié
    }

    /**
     * Résout un "abstract" et retourne une instance de l'objet.
     *
     * @param string $abstract Le nom de la classe ou de l'interface à résoudre.
     * @param array $parameters Des paramètres supplémentaires à passer au constructeur.
     * @return object L'instance résolue.
     * @throws \Exception Si l'objet ne peut pas être résolu.
     */
    public function make(string $abstract, array $parameters = []): object
    {
        // Si c'est un singleton et qu'il a déjà été instancié, retourne l'instance existante.
        if (isset($this->instances[$abstract]) && $this->instances[$abstract] !== null) {
            return $this->instances[$abstract];
        }

        $concrete = $this->bindings[$abstract] ?? $abstract; // Si pas de binding explicite, on tente d'instancier la classe elle-même.

        // Si c'est un callable, appelle la fonction pour obtenir l'instance.
        if (is_callable($concrete)) {
            $object = call_user_func($concrete, $this, ...$parameters);
        }
        // Sinon, c'est un nom de classe, tente l'auto-wiring.
        elseif (class_exists($concrete)) {
            $reflector = new \ReflectionClass($concrete);

            // Vérifie si la classe est instanciable (pas une interface, trait ou classe abstraite).
            if (!$reflector->isInstantiable()) {
                throw new \Exception("Cannot instantiate {$abstract}. It is not instantiable.");
            }

            $constructor = $reflector->getConstructor();

            // Si pas de constructeur, pas de dépendances, on instancie directement.
            if (is_null($constructor)) {
                $object = new $concrete();
            } else {
                // Résout les dépendances du constructeur via le conteneur.
                $dependencies = $this->resolveDependencies($constructor->getParameters());
                $object = $reflector->newInstanceArgs($dependencies);
            }
        } else {
            throw new \Exception("Cannot resolve {$abstract}: No binding or class found.");
        }

        // Si c'est un singleton, stocke l'instance pour les appels futurs.
        if (array_key_exists($abstract, $this->instances)) {
            $this->instances[$abstract] = $object;
        }

        return $object;
    }

    /**
     * Résout récursivement les dépendances des paramètres d'une méthode ou d'un constructeur.
     *
     * @param \ReflectionParameter[] $parameters
     * @return array
     * @throws \Exception
     */
    protected function resolveDependencies(array $parameters): array
    {
        $dependencies = [];
        foreach ($parameters as $parameter) {
            $type = $parameter->getType();

            // Si le paramètre est typé et qu'il s'agit d'une classe (pas un type scalaire comme string, int...)
            if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) {
                $dependencies[] = $this->make($type->getName()); // Résout la dépendance via le conteneur
            } elseif ($parameter->isDefaultValueAvailable()) {
                // Si une valeur par défaut est disponible, l'utilise
                $dependencies[] = $parameter->getDefaultValue();
            } else {
                // Cas non géré : type scalaire sans valeur par défaut, ou paramètre non typé.
                // Cela nécessiterait une injection manuelle ou une gestion plus complexe.
                throw new \Exception(
                    "Cannot resolve parameter '{$parameter->getName()}' of type '{$type?->getName()}' " .
                    "for automatic injection. Consider binding it in the container or providing a default value."
                );
            }
        }
        return $dependencies;
    }
}

Utilisation du Conteneur dans public/index.php et Router

Nous allons maintenant utiliser notre conteneur pour gérer l'instanciation de Request et pour injecter les contrôleurs et leurs dépendances.

// public/index.php (mis à jour avec Container)
<?php

require __DIR__ . '/../vendor/autoload.php';

use App\Router;
use App\Request;
use App\Response;
use App\Container; // Importer la classe Container
use App\Controllers\HomeController;
use App\Services\UserService; // Nous allons créer ce service

// 1. Initialiser le conteneur
$container = new Container();

// 2. Enregistrer les objets fondamentaux dans le conteneur
// On les enregistre en tant que singletons pour qu'une seule instance soit utilisée
// pour toute la durée de vie de la requête.
$container->singleton(Request::class, fn(Container $c) => new Request());
$container->singleton(Response::class, fn(Container $c) => new Response());

// 3. Enregistrer un service personnalisé (exemple)
// C'est ici que vous lieriez vos dépendances complexes, comme une connexion à la base de données, etc.
$container->singleton(UserService::class, fn(Container $c) => new UserService());

// 4. Passer le conteneur au routeur, car le routeur aura besoin de l'utiliser
// pour instancier les contrôleurs et leurs dépendances.
$router = new Router($container);

// Définition des routes (inchangées)
$router->get('/', function(Request $request) {
    $name = $request->input('name', 'Monde');
    return new Response("Bienvenue, {$name} sur notre mini-framework !");
});

$router->get('/about', [HomeController::class, 'about']);

$router->get('/api/users', [HomeController::class, 'apiUsers']); // Nouvelle route utilisant un service

// 5. Récupérer l'objet Request depuis le conteneur pour la résolution
$request = $container->make(Request::class);

try {
    // Résoudre la route via le routeur
    $response = $router->resolve($request);
    
    // Envoyer la réponse
    $response->send();

} catch (\Exception $e) {
    $statusCode = $e->getCode() ?: 500;
    (new Response($e->getMessage(), $statusCode))->send();
}

Adaptation du Router pour utiliser le Container

Nous devons modifier le Router pour qu'il reçoive le Container et l'utilise pour instancier les contrôleurs et résoudre leurs dépendances (y compris les dépendances des méthodes de contrôleur).

// src/Router.php (mis à jour avec Container et résolution des dépendances de contrôleur)
<?php

namespace App;

use App\Request;
use App\Response;
use App\Container; // Importer le Container

class Router
{
    protected array $routes = [];
    protected Container $container; // Ajouter la propriété container

    public function __construct(Container $container) // Injecter le container au constructeur
    {
        $this->container = $container;
    }

    // ... (méthodes get et post restent inchangées)

    /**
     * Résout la route actuelle et exécute l'action associée.
     *
     * @param Request $request L'objet Request courant.
     * @return Response L'objet Response généré par l'action.
     * @throws \Exception Si la route n'est pas trouvée ou l'action est invalide.
     */
    public function resolve(Request $request): Response
    {
        $uri = $request->uri();
        $method = $request->method();
        $action = $this->routes[$method][$uri] ?? null;

        if (!$action) {
            return (new Response("Route non trouvée pour $method $uri", 404));
        }

        $result = null;
        if (is_callable($action)) {
            // Pour les fonctions anonymes, on peut injecter la Request directement si elle est demandée.
            // On utilise la réflexion pour résoudre les arguments si le callable les typehint.
            $reflector = new \ReflectionFunction($action);
            $args = $this->resolveCallableDependencies($reflector->getParameters(), $request);
            $result = call_user_func_array($action, $args);
        } elseif (is_array($action) && count($action) === 2) {
            [$controllerClass, $methodName] = $action;

            if (class_exists($controllerClass) && method_exists($controllerClass, $methodName)) {
                // Utiliser le conteneur pour instancier le contrôleur et ses dépendances de constructeur
                $controller = $this->container->make($controllerClass);

                // Utiliser Reflection pour résoudre les arguments de la méthode du contrôleur
                $reflector = new \ReflectionMethod($controller, $methodName);
                $args = $this->resolveCallableDependencies($reflector->getParameters(), $request);
                
                $result = call_user_func_array([$controller, $methodName], $args);
            }
        }

        // Assurez-vous que l'action retourne une Response. Si c'est une string, enveloppez-la.
        if ($result instanceof Response) {
            return $result;
        } elseif (is_string($result)) {
            return new Response($result);
        }

        return (new Response("Résultat d'action de route invalide.", 500));
    }

    /**
     * Résout les dépendances pour un callable (fonction ou méthode de contrôleur).
     *
     * @param \ReflectionParameter[] $parameters
     * @param Request $request L'objet Request, injecté spécifiquement si demandé.
     * @return array
     */
    protected function resolveCallableDependencies(array $parameters, Request $request): array
    {
        $dependencies = [];
        foreach ($parameters as $parameter) {
            $type = $parameter->getType();
            if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) {
                $typeName = $type->getName();
                if ($typeName === Request::class) {
                    $dependencies[] = $request; // Injecte l'instance de Request actuelle
                } else {
                    $dependencies[] = $this->container->make($typeName); // Résout via le conteneur
                }
            } elseif ($parameter->isDefaultValueAvailable()) {
                $dependencies[] = $parameter->getDefaultValue();
            } else {
                throw new \Exception(
                    "Impossible de résoudre le paramètre '{$parameter->getName()}' pour l'injection automatique."
                );
            }
        }
        return $dependencies;
    }
}

Création d'un Service et adaptation du Contrôleur

Créons un service simple et voyons comment il est automatiquement injecté.

// src/Services/UserService.php
<?php

namespace App\Services;

class UserService
{
    public function getUsers(): array
    {
        // Simule une récupération de données utilisateur depuis une source (base de données, API externe, etc.)
        return ['Alice Smith', 'Bob Johnson', 'Charlie Brown'];
    }

    public function getUserById(int $id): ?string
    {
        $users = $this->getUsers();
        return $users[$id] ?? null;
    }
}

Maintenant, modifions HomeController pour qu'il utilise ce service. Le conteneur injectera UserService dans le constructeur de HomeController.

// src/Controllers/HomeController.php (mis à jour avec injection de UserService)
<?php

namespace App\Controllers;

use App\Request;
use App\Response;
use App\Services\UserService; // Importer le service

class HomeController
{
    protected UserService $userService;

    // Le conteneur va automatiquement injecter une instance de UserService ici.
    // Il sait comment la créer grâce au binding dans public/index.php.
    public function __construct(UserService $userService)
    {
        $this->userService = $userService;
    }

    public function about(Request $request): Response
    {
        $frameworkName = $request->input('name', 'Mini-Framework');
        return new Response("À propos de {$frameworkName}. C'est un projet d'apprentissage.");
    }

    public function apiUsers(Request $request): Response
    {
        // Utilise le service UserService pour récupérer les utilisateurs
        $users = $this->userService->getUsers();
        return Response::json(['users' => $users]);
    }
}

Assurez-vous d'avoir bien la route /api/users dans public/index.php. Accédez à http://localhost:8000/api/users pour voir le résultat JSON.


Étape 5 : Le Système de Vues (très simple)

Dans une application web, il est crucial de séparer la logique de l'application de sa présentation. C'est le rôle du système de vues. Pour notre mini-framework, nous allons créer un système très rudimentaire basé sur de simples fichiers PHP.

Création de la classe View

Créez le fichier src/View.php.

// src/View.php
<?php

namespace App;

class View
{
    protected string $path;
    protected array $data;

    public function __construct(string $path, array $data = [])
    {
        // Chemin complet vers le fichier de vue.
        // Nous supposons que les vues sont dans 'resources/views/'.
        $this->path = __DIR__ . '/../resources/views/' . $path . '.php';
        $this->data = $data;
    }

    /**
     * Rend la vue en incluant le fichier PHP et en capturant sa sortie.
     * Les données passées au constructeur sont extraites et deviennent des variables locales dans la vue.
     * @return string Le contenu HTML généré par la vue.
     * @throws \Exception Si le fichier de vue n'existe pas.
     */
    public function render(): string
    {
        if (!file_exists($this->path)) {
            throw new \Exception("Fichier de vue non trouvé: {$this->path}");
        }

        // Extrait les éléments du tableau $data en variables locales dans la portée actuelle (pour la vue).
        // Par exemple, si $data = ['title' => 'Mon Titre'], alors $title sera disponible dans la vue.
        extract($this->data);

        // Démarre la capture de la sortie tampon (output buffering).
        // Tout ce qui est "echo" ou "print" dans le fichier de vue sera capturé au lieu d'être directement envoyé au navigateur.
        ob_start();
        
        require $this->path; // Inclut le fichier de vue

        // Récupère le contenu du tampon de sortie et l'efface.
        return ob_get_clean();
    }

    /**
     * Méthode statique pour créer une instance de View.
     * Permet d'écrire View::make('welcome', ['data' => '...'])
     * @param string $path
     * @param array $data
     * @return self
     */
    public static function make(string $path, array $data = []): self
    {
        return new self($path, $data);
    }
}

Création d'un répertoire de vues

Créez le dossier resources/views à la racine de votre projet. À l'intérieur, créez un fichier welcome.php.

// resources/views/welcome.php
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><?= $title ?? 'Bienvenue' ?></title>
    <style>
        body { font-family: sans-serif; margin: 2em; background-color: #f4f4f4; color: #333; }
        .container { max-width: 800px; margin: 0 auto; padding: 20px; background-color: #fff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
        h1 { color: #0056b3; }
        p { line-height: 1.6; }
        footer { margin-top: 2em; font-size: 0.8em; color: #666; text-align: center; }
    </style>
</head>
<body>
    <div class="container">
        <h1><?= $heading ?? 'Bienvenue !' ?></h1>
        <p><?= $message ?? 'Ceci est la page d\'accueil de votre mini-framework.' ?></p>
        <?php if (isset($items) && is_array($items)): ?>
            <h2>Liste :</h2>
            <ul>
                <?php foreach ($items as $item): ?>
                    <li><?= htmlspecialchars($item) ?></li>
                <?php endforeach; ?>
            </ul>
        <?php endif; ?>
    </div>
    <footer>
        <p>&copy; <?= date('Y') ?> Mon Mini-Framework. Tous droits réservés.</p>
    </footer>
</body>
</html>

Utilisation dans HomeController

Nous pouvons maintenant utiliser notre système de vues pour renvoyer des pages HTML dynamiques.

// src/Controllers/HomeController.php (ajout de la méthode welcome)
<?php

namespace App\Controllers;

use App\Request;
use App\Response;
use App\Services\UserService;
use App\View; // Importer la classe View

class HomeController
{
    protected UserService $userService;

    public function __construct(UserService $userService)
    {
        $this->userService = $userService;
    }

    public function about(Request $request): Response
    {
        $frameworkName = $request->input('name', 'Mini-Framework');
        return new Response("À propos de {$frameworkName}. C'est un projet d'apprentissage.");
    }

    public function apiUsers(Request $request): Response
    {
        $users = $this->userService->getUsers();
        return Response::json(['users' => $users]);
    }

    /**
     * Affiche la page d'accueil avec une vue.
     */
    public function welcome(Request $request): Response
    {
        $name = $request->input('name', 'Visiteur');
        $users = $this->userService->getUsers(); // Exemple d'utilisation du service
        
        $viewContent = View::make('welcome', [
            'heading' => "Bonjour, {$name} !",
            'message' => "Heureux de vous revoir sur notre mini-framework. Voici quelques utilisateurs :",
            'items' => $users // Passer les utilisateurs à la vue
        ])->render();

        return new Response($viewContent);
    }
}

Enfin, mettons à jour la route par défaut dans public/index.php pour utiliser notre nouvelle méthode welcome :

// public/index.php (modifier la route '/')
// ...
$router->get('/', [HomeController::class, 'welcome']);
// ...

Accédez à http://localhost:8000/ pour voir votre page de bienvenue rendue par le système de vues.


Liens avec Laravel

Vous avez maintenant mis en place les fondations d'un mini-framework. Il est temps de voir comment ces concepts se manifestent dans Laravel, un framework bien plus mature et riche en fonctionnalités.

  • Front Controller : Laravel utilise aussi un public/index.php comme point d'entrée unique. C'est le premier fichier exécuté qui amorce tout le framework.
  • Autoloading : Laravel s'appuie entièrement sur Composer et la norme PSR-4 pour le chargement automatique de toutes ses classes et celles de vos applications.
  • Routing : Le système de routage de Laravel (Route::get('/users', [UserController::class, 'index']);) est une version beaucoup plus sophistiquée de notre Router. Il gère les paramètres de route, les middlewares, les groupes de routes, les noms de routes, et bien plus encore, mais le principe de base de mapper une URI à une action reste le même.
  • Request/Response : Laravel utilise des objets Illuminate\Http\Request et Illuminate\Http\Response (qui s'appuient sur le composant Symfony HttpFoundation) pour encapsuler toutes les interactions HTTP. Ces objets sont beaucoup plus riches en méthodes utilitaires.
  • Conteneur IoC/DI : Le cœur battant de Laravel est son conteneur de services, Illuminate\Container\Container. Il gère l'instanciation et l'injection de toutes les dépendances dans l'application, y compris les contrôleurs, les services, les middlewares, les listeners, etc. Il dispose de fonctionnalités avancées comme l'auto-wiring (résolution automatique des dépendances sans binding explicite), le contexte d'injection, et le binding d'interfaces à des implémentations.
  • Vues : Laravel utilise Blade, son propre moteur de templates, qui est bien plus qu'un simple include. Blade offre l'héritage de templates (@extends, @section), des directives (@if, @foreach), des composants, et la compilation en simples fichiers PHP pour la performance. Cependant, le principe sous-jacent est toujours de générer du HTML à partir de données PHP.
  • Service Providers : Dans Laravel, l'enregistrement des "bindings" (les règles pour créer des objets) dans le conteneur IoC se fait principalement via les Service Providers. Ils permettent d'organiser et de "démarrer" les différentes parties du framework et de votre application.

Conclusion

Félicitations ! Vous avez accompli un pas significatif dans la compréhension des rouages internes des frameworks web. Ce voyage à travers la création d'un mini-framework vous a permis de :

  • Démystifier les rouages internes : Vous avez vu comment une requête web est reçue, routée, transformée en objets, traitée par des contrôleurs utilisant des services injectés, et finalement convertie en une réponse HTTP.
  • Apprécier la complexité des grands frameworks : Vous réalisez maintenant l'ingénierie et la multitude de décisions de conception nécessaires pour bâtir des outils aussi robustes et flexibles que Laravel. Ce que nous avons construit est une esquisse, Laravel est une œuvre complète !
  • Renforcer vos compétences en architecture logicielle : Des concepts comme l'abstraction, l'Inversion de Contrôle, l'Injection de Dépendances et la séparation des préoccupations ne sont plus de simples mots-clés, mais des principes concrets que vous avez implémentés.
  • Devenir un développeur plus éclairé : Vous savez maintenant où chercher quand quelque chose ne va pas dans Laravel, comment étendre ses fonctionnalités de manière appropriée, et comment ses composants interagissent.

Ces concepts ne sont pas spécifiques à Laravel ou même au PHP ; ils sont des principes fondamentaux de la conception logicielle moderne, applicables à n'importe quel langage ou environnement de développement. Maîtriser ces bases vous rendra non seulement un meilleur développeur Laravel, mais aussi un architecte logiciel plus polyvalent et capable de comprendre et d'adapter n'importe quel système complexe. Continuez à explorer, à construire et à casser pour mieux comprendre !