Contrôleurs
Des classes ordinaires, des réponses explicites.
Un contrôleur est une classe dont les méthodes publiques traitent les requêtes et retournent une Response. Le routage décide quelle méthode répond à quelle URL. Étendre Zephyrus\Controller\Controller est facultatif, mais cela vous donne les assistants de réponse et les points d’entrée de cycle de vie ci-dessous.
Un premier contrôleur
<?php
declare(strict_types=1);
namespace App\Controllers;
use Zephyrus\Controller\Controller;
use Zephyrus\Http\Request;
use Zephyrus\Http\Response;
use Zephyrus\Routing\Attribute\Get;
use Zephyrus\Routing\Attribute\Post;
use Zephyrus\Routing\Attribute\Root;
#[Root('/projects')]
final class ProjectController extends Controller
{
#[Get('/{id}', constraints: ['id' => '\d+'])]
public function show(int $id): Response
{
return $this->json(['id' => $id]);
}
#[Post('/')]
public function store(Request $request): Response
{
return $this->created(['name' => $request->body()->get('name')]);
}
}
Un gestionnaire prend les paramètres dont il a besoin : une Request, et les paramètres de route par leur nom (int $id). Consultez Paramètres de route pour les règles de résolution. Un gestionnaire doit retourner une Response.
Une classe qui n’étend pas Controller fonctionne aussi, pourvu que ses méthodes de gestion retournent une Response.
Assistants de réponse
Chaque assistant est une méthode protégée qui retourne une Response (voir HTTP pour la réponse elle-même).
| Assistant | Réponse |
|---|---|
json(array $payload, int $status = 200) |
Corps JSON, application/json |
created(array $payload) |
Corps JSON avec le statut 201 |
respond(array $payload, int $status) |
Corps JSON avec le statut de votre choix |
text(string $body, int $status = 200) |
Corps text/plain |
noContent() |
204, corps vide |
redirect(string $url, int $status = 302) |
Redirection vers une cible fixe |
localRedirect(mixed $target, string $fallback = '/', int $status = 302) |
Redirection vers une cible lue dans la requête |
abort(int $status, string $body = '') |
Réponse HTML avec ce statut |
abortJson(int $status, array $payload) |
Réponse JSON avec ce statut |
redirect() est destiné aux cibles que vous écrivez. Pour tout ce qui provient de la requête (un paramètre de requête next, un champ de formulaire), utilisez localRedirect() : il ne suit qu’un chemin local et renvoie le visiteur vers la valeur de repli dans le cas contraire.
#[Get('/back')]
public function back(Request $request): Response
{
return $this->localRedirect($request->query('next'), '/account/');
}
?next=//evil.example redirige vers /account/ ; ?next=/account/settings redirige vers /account/settings.
Valider les entrées
validate() exécute un FormValidator sur un tableau et retourne son sac d’erreurs. Lorsqu’un champ échoue, il lance une ValidationException, à laquelle l’application répond par un 422 à moins que vous ne la traitiez (voir Validation et Erreurs).
use Zephyrus\Validation\FormValidator;
use Zephyrus\Validation\Rules;
#[Post('/email')]
public function changeEmail(Request $request): Response
{
$form = new FormValidator(['email' => [Rules::required(), Rules::email()]]);
$this->validate($form, $request->body()->all());
return $this->json(['email' => $request->body()->get('email')]);
}
Points d’entrée de cycle de vie
Controller implémente ControllerLifecycleInterface : before(Request $request): ?Response s’exécute avant le gestionnaire et after(Request $request, Response $response): Response s’exécute après. L’interface est le contrat ; tout contrôleur peut l’implémenter sans étendre Controller.
L’ordre est before(), le gestionnaire, after(). Une Response retournée par before() est envoyée telle quelle : le gestionnaire et after() sont ignorés. Une exception lancée par le gestionnaire ignore aussi after().
final class AccountController extends Controller
{
public function before(Request $request): ?Response
{
parent::before($request);
if ($request->headers()->get('X-Api-Key') !== 'demo-key') {
return $this->respond(['error' => 'Unauthorized'], 401);
}
return null;
}
public function after(Request $request, Response $response): Response
{
return $response->withHeader('Cache-Control', 'no-store');
}
#[Get('/account')]
public function show(): Response
{
return $this->json(['path' => $this->request->path()]);
}
}
Le before() par défaut range la requête dans $this->request. Une redéfinition doit d’abord appeler parent::before($request), sinon $this->request reste non initialisé. Les points d’entrée s’appliquent à un seul contrôleur ; la logique partagée par plusieurs routes relève des middlewares, qui voient aussi les réponses 404 et 405.
Le 401 ci-dessus ignore after(), il ne porte donc aucun en-tête Cache-Control. Décorez les réponses d’erreur là où vous les construisez, ou faites-le dans un middleware.
Rendre des vues
Un contrôleur qui rend des gabarits utilise le trait RenderResponses, qui ajoute render(), renderWith() et html() :
use Zephyrus\Rendering\RenderResponses;
final class PageController extends Controller
{
use RenderResponses;
#[Get('/pages/{id}', constraints: ['id' => '\d+'])]
public function show(int $id): Response
{
return $this->render('pages/show', ['id' => $id]);
}
}
ApplicationBuilder::withRenderEngine() remet le moteur à chaque contrôleur qui utilise le trait. Les moteurs, la recherche de gabarits et les assistants Latte sont traités dans Vues.
Injection par le constructeur
Par défaut, un contrôleur est construit sans argument de constructeur. Passez un conteneur au builder de l’application et les dépendances de son constructeur sont résolues pour vous :
use Zephyrus\Container\Container;
$app = ApplicationBuilder::create()
->withRouter($router)
->withContainer(new Container())
->build();
final class AccountController extends Controller
{
public function __construct(private readonly Clock $clock)
{
}
}
Le conteneur construit Clock automatiquement lorsqu’il s’agit d’une classe concrète dont les arguments sont résolvables. Enregistrez les interfaces et les services partagés avec bind(), singleton() et instance() : voir Conteneur et événements. withControllerFactory(fn (string $class): object => ...) permet de brancher toute autre manière de construire les contrôleurs.
Bon à savoir
- Une redéfinition de
before()doit appelerparent::before($request), sinon$this->requestn’est pas initialisé lorsque le gestionnaire le lit. - Une
Responseprovenant debefore()ignoreafter(); une exception du gestionnaire l’ignore aussi. - N’enregistrez pas un contrôleur comme singleton du conteneur s’il garde un état propre à la requête, comme
$this->request: la même instance servirait toutes les requêtes du processus. - Utilisez
localRedirect()pour toute cible de redirection qui provient de la requête ;redirect()fait confiance à son argument. abort()envoie un corps HTML etabortJson()un corps JSON. Choisissez celui qu’attend votre client.- Un enregistrement manquant est un 404 :
return $this->abort(404), ou lancezRouteNotFoundExceptiondepuis un code plus profond (voir Routage).