Concepts fondamentaux

Concepts fondamentaux

Ce que fait exactement Niang\Core\Application entre la requête HTTP et la réponse envoyée, comment le Container résout vos dépendances, et où brancher du code au démarrage.

Cycle de vie d'une requête

public/index.php crée une Application, charge les routes, puis appelle run(). Le constructeur d'Application fait, dans l'ordre :

  1. Charge .env (ou .env.testing si APP_ENV=testing) et config/*.php.
  2. Crée le Container et le Router.
  3. Enregistre dans le Container, comme singletons : Router, Container, Psr\Container\ContainerInterface, Application elle-même, Logger et Psr\Log\LoggerInterface.
  4. Configure error_reporting / display_errors selon APP_DEBUG.
  5. Instancie chaque Service Provider listé dans config('app.providers'), appelle register() sur tous, puis boot() sur tous — jamais register() puis boot() provider par provider (voir Service Providers).

run() puis handle() font le reste :

packages/foundation/src/Application.php
public function run(): void
{
    Session::start();
    $this->handle(Request::capture())->send();
}

public function handle(Request $request): Response
{
    $startedAt = hrtime(true);
    DB::resetQueryCount();

    try {
        $response = $this->router->dispatch($request, $this->container);
    } catch (\Throwable $e) {
        $response = Handler::render($e, $request, $startedAt);
    }

    $response = $this->applySecurityHeaders($response);
    $response = DebugToolbar::inject($response, $startedAt);

    return $this->compressIfSupported($response, $request);
}

Ce qui donne, en une phrase, le trajet complet d'une requête :

public/index.php → Application → Router::dispatch() → middleware de la route (dans l'ordre, en pipeline) → Controller → Response, puis applySecurityHeaders() (en-têtes de config/security.php), DebugToolbar::inject() et une compression gzip conditionnelle avant l'envoi.

Toute exception levée pendant le dispatch — y compris une HttpException issue de abort() ou une exception de validation — est interceptée une seule fois ici et transformée en réponse par Handler::render() ; le routeur lui-même ne « sait » pas afficher une page d'erreur.

Événements du framework

Pour une extension ou un paquet qui doit intervenir à chaque requête, sans toucher au code de l'application :

ÉvénementMoment
Events\ApplicationBootedaprès le boot() de tous les Service Providers
Events\RequestReceiveddébut de handle(), avant maintenance et routeur
Events\RouteMatchedroute trouvée, avant les middlewares
Events\ResponsePreparedréponse prête, juste avant l'envoi (encore modifiable)
Events\RequestTerminatedaprès l'envoi (avec PHP-FPM, le visiteur n'attend plus)
php
Event::listen(ResponsePrepared::class, fn (ResponsePrepared $e) => $e->response->header('X-Version', '2.1'));

Émis seulement s'ils sont écoutés : aucun coût sinon.

Le Container

Niang\Core\Container implémente Psr\Container\ContainerInterface (get()/has()) et ajoute deux méthodes d'enregistrement :

bind() et singleton()

php
// bind() : une closure appelée à chaque résolution, elle reçoit le container.
$container->bind(PaymentGateway::class, fn ($c) => new StripeGateway($c->make(Config::class)));

// singleton() : une instance déjà construite, retournée telle quelle à chaque appel.
$container->singleton(Logger::class, new Logger());

Sans bind() explicite, make(SomeClass::class) instancie SomeClass directement par réflexion (voir ci-dessous) — un binding n'est nécessaire que pour choisir une implémentation concrète derrière une interface, ou construire un objet qui a besoin d'arguments non auto-résolubles.

Classes, singletons paresseux et liaisons contextuelles

php
$container->bind(PaymentGateway::class, StripeGateway::class);        // interface -> classe
$container->singleton(CacheManager::class);                           // une instance, créée au premier usage
$container->instance(Clock::class, new FrozenClock('2026-01-01'));     // objet déjà construit
$container->when(RefundController::class)->needs(PaymentGateway::class)->give(PaypalGateway::class);

bind() crée une nouvelle instance à chaque résolution ; singleton() et instance() renvoient toujours la même. La liaison contextuelle choisit une implémentation selon la classe qui la demande, pour le constructeur comme pour l'injection dans les méthodes.

Injection par réflexion

make() et call() inspectent les paramètres d'un constructeur ou d'une méthode avec ReflectionParameter et résolvent chacun, dans cet ordre :

  1. Type déclaré et non natif (une classe/interface) : recherché parmi les valeurs déjà connues du type (ex. la Request courante), sinon résolu récursivement via make(). Un FormRequest est un cas particulier : construit à partir de la requête courante puis validé (voir Container::makeFormRequest()), jamais instancié à vide.
  2. Une valeur nommée fournie explicitement (ex. les paramètres de route passés à call()).
  3. Un paramètre de route de même nom que le paramètre de la méthode (function show($id) reçoit directement le {id} de l'URL).
  4. La valeur par défaut du paramètre, si elle existe.
  5. null, si le type l'autorise.
  6. Sinon, ContainerException — avec le nom du paramètre, son type, et le contexte (classe/méthode) dans le message.

C'est ce mécanisme qui permet à un contrôleur de déclarer index(Request $request) ou show(string $id) sans configuration : le Router appelle toute action via $container->call([$controller, $method], ['request' => $request]).

Une dépendance circulaire (A demande B qui demande A) est détectée avant la récursion infinie et lève une ContainerException listant la chaîne complète. Résoudre une interface ou une classe abstraite sans bind() préalable échoue avec un message qui rappelle comment l'enregistrer.

Service Providers

Un Service Provider étend Niang\Core\ServiceProvider et redéfinit register() (bindings dans le Container) et/ou boot() (écouteurs d'événements, initialisation — exécuté une fois que tous les providers ont fini leur register(), donc un provider peut compter sur un binding posé par un autre) :

app/Providers/AppServiceProvider.php

namespace App\Providers;

use Niang\Core\Event;
use Niang\Core\Log;
use Niang\Core\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Event::listen('user.registered', function (array $user): void {
            Log::info('Nouvel utilisateur inscrit : {email}', ['email' => $user['email']]);
        });
    }
}

Il est déclaré dans config/app.php, qui liste tous les providers du projet :

config/app.php
'providers' => [
    App\Providers\AppServiceProvider::class,
],

Points d'extension

Les composants du framework ne se connaissent pas directement : Log ignore tout des traces, Cache du multi-locataire, Event de la file d'attente. Chacun expose un point d'extension, et Application::wire() les assemble au démarrage (ADR 0012) — c'est ce qui permettra de séparer le framework en paquets indépendants. Une application peut s'en servir aussi, par exemple dans le boot() d'un Service Provider :

php
// Ajouter l'identifiant de l'utilisateur à chaque ligne de log
Log::contextUsing('utilisateur', fn (): array => ['user_id' => Auth::id()]);

// Un en-tête sur chaque appel sortant (webhooks, OAuth, S3...)
Http\Client::headersUsing('version', fn (): array => ['X-App-Version' => '2.3.0']);

// Noter la langue courante sur chaque job, et la rétablir dans le worker
Queue::stampUsing('langue', fn (Job $job) => $job->context['locale'] = Lang::locale());
Queue::wrapUsing('langue', function (Job $job, Closure $next): void {
    Lang::setLocale($job->context['locale'] ?? 'fr');
    $next();
});
Point d'extensionRôleBranché par le framework sur
Log::contextUsing($nom, $fournisseur)contexte ajouté à chaque messagerequest_id de la requête
Http\Client::headersUsing($nom, $fournisseur)en-têtes des appels sortantstraceparent
Event::queueUsing($file)différer un écouteur ShouldQueueQueue (sans file : exécution immédiate)
Cache::prefixUsing($prefixe)préfixe de chaque clélocataire courant
Model::tenantScopeUsing($portee)filtre des modèles $tenantScopedTenancy
Metrics::isolateUsing($enveloppe)contexte des lectures et écritures de métriqueshors locataire
Queue::stampUsing(), wrapUsing(), afterUsing()à la mise en file, autour de l'exécution, après chaque jobrequête et locataire d'origine, métriques
Container::resolveUsing($classeDeBase, $resolveur)construire toute une famille de classesFormRequest (par le Router)

Chaque point d'extension nommé remplace un fournisseur du même nom : un second appel ne s'ajoute pas au premier.

⏱ 12.61 ms 🗄 0 requête(s) SQL 🧠 4.00 MB ↩ 200