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 :
- Charge
.env(ou.env.testingsiAPP_ENV=testing) etconfig/*.php. - Crée le
Containeret leRouter. - Enregistre dans le Container, comme singletons :
Router,Container,Psr\Container\ContainerInterface,Applicationelle-même,LoggeretPsr\Log\LoggerInterface. - Configure
error_reporting/display_errorsselonAPP_DEBUG. - Instancie chaque Service Provider listé dans
config('app.providers'), appelleregister()sur tous, puisboot()sur tous — jamaisregister()puisboot()provider par provider (voir Service Providers).
run() puis handle() font le reste :
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énement | Moment |
|---|---|
Events\ApplicationBooted | après le boot() de tous les Service Providers |
Events\RequestReceived | début de handle(), avant maintenance et routeur |
Events\RouteMatched | route trouvée, avant les middlewares |
Events\ResponsePrepared | réponse prête, juste avant l'envoi (encore modifiable) |
Events\RequestTerminated | après l'envoi (avec PHP-FPM, le visiteur n'attend plus) |
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()
// 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
$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 :
- Type déclaré et non natif (une classe/interface) : recherché parmi les valeurs déjà connues du type (ex. la
Requestcourante), sinon résolu récursivement viamake(). UnFormRequestest un cas particulier : construit à partir de la requête courante puis validé (voirContainer::makeFormRequest()), jamais instancié à vide. - Une valeur nommée fournie explicitement (ex. les paramètres de route passés à
call()). - 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). - La valeur par défaut du paramètre, si elle existe.
null, si le type l'autorise.- 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) :
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 :
'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 :
// 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'extension | Rôle | Branché par le framework sur |
|---|---|---|
Log::contextUsing($nom, $fournisseur) | contexte ajouté à chaque message | request_id de la requête |
Http\Client::headersUsing($nom, $fournisseur) | en-têtes des appels sortants | traceparent |
Event::queueUsing($file) | différer un écouteur ShouldQueue | Queue (sans file : exécution immédiate) |
Cache::prefixUsing($prefixe) | préfixe de chaque clé | locataire courant |
Model::tenantScopeUsing($portee) | filtre des modèles $tenantScoped | Tenancy |
Metrics::isolateUsing($enveloppe) | contexte des lectures et écritures de métriques | hors locataire |
Queue::stampUsing(), wrapUsing(), afterUsing() | à la mise en file, autour de l'exécution, après chaque job | requête et locataire d'origine, métriques |
Container::resolveUsing($classeDeBase, $resolveur) | construire toute une famille de classes | FormRequest (par le Router) |
Chaque point d'extension nommé remplace un fournisseur du même nom : un second appel ne s'ajoute pas au premier.