Presenters
Nous allons découvrir comment écrire des presenters et des templates en Nette. Après lecture, vous saurez :
- comment fonctionne un presenter
- ce que sont les paramètres persistants
- comment les templates sont rendus
Nous savons déjà qu'un presenter est une classe qui représente une page web spécifique d'une application, par ex. la page d'accueil ; un produit dans une boutique en ligne ; un formulaire de connexion ; un flux sitemap, etc. Une application peut avoir d'un à des milliers de presenters. Dans d'autres frameworks, on les appelle aussi controllers.
Habituellement, par le terme presenter, on entend un descendant de la classe Nette\Application\UI\Presenter, qui est adapté à la génération d'interfaces web et auquel nous nous consacrerons dans le reste de ce chapitre. Au sens général, un presenter est n'importe quel objet implémentant l'interface Nette\Application\IPresenter.
Cycle de vie du presenter
La tâche du presenter est de traiter une requête et de retourner une réponse (qui peut être une page HTML, une image, une redirection, etc.).
Donc, au début, une requête lui est transmise. Ce n'est pas directement une requête HTTP, mais un objet Nette\Application\Request, dans lequel la requête HTTP a été transformée à l'aide du routeur. Nous n'interagissons généralement pas directement avec cet objet, car le presenter délègue intelligemment le traitement de la requête à d'autres méthodes, que nous allons présenter maintenant.
L'image représente la liste des méthodes qui sont appelées successivement de haut en bas, si elles existent. Aucune d'elles n'est obligatoire, nous pouvons avoir un presenter complètement vide sans une seule méthode et construire un site web statique simple dessus.
__construct()
Le constructeur n'appartient pas tout à fait au cycle de vie du presenter, car il est appelé au moment de la création de l'objet. Mais nous le mentionnons en raison de son importance. Le constructeur (avec la méthode inject) sert à passer les dépendances.
Un presenter ne devrait pas gérer la logique métier de l'application, écrire et lire dans la base de données, effectuer des
calculs, etc. C'est le rôle des classes de la couche que nous appelons modèle. Par exemple, la classe
ArticleRepository
peut être responsable du chargement et de la sauvegarde des articles. Pour que le presenter puisse
travailler avec elle, il la reçoit via l'injection
de dépendances :
class ArticlePresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private ArticleRepository $articles,
) {
}
}
startup()
Immédiatement après réception de la requête, la méthode startup()
est appelée. Vous pouvez l'utiliser pour
initialiser les propriétés, vérifier les permissions utilisateur, etc. Il est requis que la méthode appelle toujours le parent
parent::startup()
.
action<Action>(args...)
Analogue à la méthode render<View>()
. Alors que render<View>()
est destinée à
préparer les données pour un template spécifique qui sera ensuite rendu, action<Action>()
traite la requête
sans lien avec le rendu du template. Par exemple, elle traite les données, connecte ou déconnecte l'utilisateur, etc., puis redirige ailleurs.
Il est important que action<Action>()
soit appelée avant render<View>()
, nous pouvons
donc éventuellement y modifier le déroulement ultérieur, c'est-à-dire changer le template qui sera rendu, ainsi que la
méthode render<View>()
qui sera appelée. Ceci est fait en utilisant setView('autreVue')
.
Les paramètres de la requête sont passés à la méthode. Il est possible et recommandé de spécifier les types des
paramètres, par ex. actionShow(int $id, ?string $slug = null)
– si le paramètre id
est manquant ou
s'il n'est pas un entier, le presenter retournera une erreur 404 et terminera son
activité.
handle<Signal>(args...)
La méthode traite les signaux, que nous découvrirons dans le chapitre consacré aux composants. Elle est en effet principalement destinée aux composants et au traitement des requêtes AJAX.
Les paramètres de la requête sont passés à la méthode, comme dans le cas de action<Action>()
, y compris
la vérification de type.
beforeRender()
La méthode beforeRender
, comme son nom l'indique, est appelée avant chaque méthode
render<View>()
. Elle est utilisée pour la configuration commune du template, passer des variables au
layout, etc.
render<View>(args...)
L'endroit où nous préparons le template pour le rendu ultérieur, lui passons des données, etc.
Les paramètres de la requête sont passés à la méthode, comme dans le cas de action<Action>()
, y compris
la vérification de type.
public function renderShow(int $id): void
{
// nous obtenons les données du modèle et les passons au template
$this->template->article = $this->articles->getById($id);
}
afterRender()
La méthode afterRender
, comme son nom l'indique encore une fois, est appelée après chaque méthode
render<View>()
. Elle est utilisée plutôt exceptionnellement.
shutdown()
Appelée à la fin du cycle de vie du presenter.
Un bon conseil avant de continuer. Comme vous pouvez le voir, un presenter peut gérer plusieurs actions/vues,
c'est-à-dire avoir plusieurs méthodes render<View>()
. Mais nous recommandons de concevoir des presenters avec
une seule ou le moins d'actions possible.
Envoi de la réponse
La réponse d'un presenter est généralement le rendu d'un template avec une page HTML, mais elle peut aussi être l'envoi d'un fichier, de JSON ou même une redirection vers une autre page.
À tout moment du cycle de vie, nous pouvons envoyer une réponse avec l'une des méthodes suivantes et ainsi terminer le presenter :
redirect()
,redirectPermanent()
,redirectUrl()
etforward()
redirigenterror()
termine le presenter en raison d'une erreursendJson($data)
termine le presenter et envoie les données au format JSONsendTemplate()
termine le presenter et rend immédiatement le templatesendResponse($response)
termine le presenter et envoie une réponse personnaliséeterminate()
termine le presenter sans réponse
Si vous n'appelez aucune de ces méthodes, le presenter procédera automatiquement au rendu du template. Pourquoi ? Parce que dans 99 % des cas, nous voulons rendre un template, donc le presenter considère ce comportement comme celui par défaut et veut nous faciliter le travail.
Création de liens
Le presenter dispose de la méthode link()
, à l'aide de laquelle il est possible de créer des liens URL vers
d'autres presenters. Le premier paramètre est le presenter & action cible, suivi des arguments passés, qui peuvent être
spécifiés sous forme de tableau :
$url = $this->link('Product:show', $id);
$url = $this->link('Product:show', [$id, 'lang' => 'cs']);
Dans le template, les liens vers d'autres presenters & actions sont créés de cette manière :
<a n:href="Product:show $id">détail du produit</a>
Au lieu d'une URL réelle, écrivez simplement la paire connue Presenter:action
et spécifiez d'éventuels
paramètres. L'astuce réside dans n:href
, qui indique que cet attribut sera traité par Latte et générera une URL
réelle. Dans Nette, vous n'avez donc pas du tout à penser aux URL, seulement aux presenters et aux actions.
Plus d'informations peuvent être trouvées dans le chapitre Création de liens URL.
Redirection
Pour passer à un autre presenter, les méthodes redirect()
et forward()
sont utilisées, qui ont une
syntaxe très similaire à la méthode link().
La méthode forward()
passe immédiatement au nouveau presenter sans redirection HTTP :
$this->forward('Product:show');
Exemple de redirection dite temporaire avec le code HTTP 302 (ou 303 si la méthode de la requête actuelle est POST) :
$this->redirect('Product:show', $id);
Vous obtenez une redirection permanente avec le code HTTP 301 comme ceci :
$this->redirectPermanent('Product:show', $id);
Il est possible de rediriger vers une autre URL en dehors de l'application avec la méthode redirectUrl()
. Le code
HTTP peut être spécifié comme deuxième paramètre, la valeur par défaut est 302 (ou 303 si la méthode de la requête
actuelle est POST) :
$this->redirectUrl('https://nette.org');
La redirection termine immédiatement l'activité du presenter en levant une exception de terminaison silencieuse appelée
Nette\Application\AbortException
.
Avant la redirection, il est possible d'envoyer des messages flash, c'est-à-dire des messages qui seront affichés dans le template après la redirection.
Messages Flash
Ce sont des messages informant généralement du résultat d'une opération. Une caractéristique importante des messages flash est qu'ils sont disponibles dans le template même après une redirection. Même après affichage, ils restent actifs pendant 30 secondes supplémentaires – par exemple, au cas où l'utilisateur rafraîchirait la page en raison d'une erreur de transmission – le message ne disparaîtra donc pas immédiatement.
Il suffit d'appeler la méthode flashMessage() et le
presenter se chargera de la transmettre au template. Le premier paramètre est le texte du message et le deuxième paramètre
facultatif est son type (error, warning, info, etc.). La méthode flashMessage()
retourne une instance du message
flash, à laquelle des informations supplémentaires peuvent être ajoutées.
$this->flashMessage('L\'élément a été supprimé.');
$this->redirect(/* ... */); // et nous redirigeons
Dans le template, ces messages sont disponibles dans la variable $flashes
sous forme d'objets
stdClass
, qui contiennent les propriétés message
(texte du message), type
(type de
message) et peuvent contenir les informations utilisateur mentionnées précédemment. Nous les rendons par exemple comme
ceci :
{foreach $flashes as $flash}
<div class="flash {$flash->type}">{$flash->message}</div>
{/foreach}
Erreur 404 et autres
Si la requête ne peut pas être satisfaite, par exemple parce que l'article que nous voulons afficher n'existe pas dans la
base de données, nous levons une erreur 404 avec la méthode
error(?string $message = null, int $httpCode = 404)
.
public function renderShow(int $id): void
{
$article = $this->articles->getById($id);
if (!$article) {
$this->error();
}
// ...
}
Le code d'erreur HTTP peut être passé comme deuxième paramètre, la valeur par défaut est 404. La méthode fonctionne en
levant une exception Nette\Application\BadRequestException
, après quoi Application
passe le contrôle
à l'error-presenter. C'est un presenter dont la tâche est d'afficher une page informant de l'erreur survenue. La configuration
de l'error-preseter se fait dans la configuration
application.
Envoi de JSON
Exemple de méthode d'action qui envoie des données au format JSON et termine le presenter :
public function actionData(): void
{
$data = ['hello' => 'nette'];
$this->sendJson($data);
}
Paramètres de la requête
Le presenter ainsi que chaque composant obtiennent leurs paramètres de la requête HTTP. Vous pouvez connaître leur valeur
avec la méthode getParameter($name)
ou getParameters()
. Les valeurs sont des chaînes ou des tableaux
de chaînes, il s'agit essentiellement de données brutes obtenues directement de l'URL.
Pour plus de commodité, nous recommandons de rendre les paramètres accessibles via une propriété. Il suffit de les marquer
avec l'attribut #[Parameter]
:
use Nette\Application\Attributes\Parameter; // cette ligne est importante
class HomePresenter extends Nette\Application\UI\Presenter
{
#[Parameter]
public string $theme; // doit être public
}
Nous recommandons d'indiquer également le type de données pour la propriété (par ex. string
) et Nette
transtypera automatiquement la valeur en conséquence. Les valeurs des paramètres peuvent également être validées.
Lors de la création d'un lien, la valeur des paramètres peut être définie directement :
<a n:href="Home:default theme: dark">cliquer</a>
Paramètres persistants
Les paramètres persistants sont utilisés pour maintenir l'état entre différentes requêtes. Leur valeur reste la même
même après avoir cliqué sur un lien. Contrairement aux données de session, ils sont transmis dans l'URL. Et cela de manière
entièrement automatique, il n'est donc pas nécessaire de les spécifier explicitement dans link()
ou
n:href
.
Exemple d'utilisation ? Vous avez une application multilingue. La langue actuelle est un paramètre qui doit constamment faire
partie de l'URL. Mais il serait incroyablement fastidieux de l'indiquer dans chaque lien. Vous en faites donc un paramètre
persistant lang
et il sera transmis automatiquement. Génial !
La création d'un paramètre persistant est extrêmement simple dans Nette. Il suffit de créer une propriété publique et de
la marquer avec un attribut : (auparavant, /** @persistent */
était utilisé)
use Nette\Application\Attributes\Persistent; // cette ligne est importante
class ProductPresenter extends Nette\Application\UI\Presenter
{
#[Persistent]
public string $lang; // doit être public
}
Si $this->lang
a la valeur, par exemple, 'en'
, alors les liens créés à l'aide de
link()
ou n:href
contiendront également le paramètre lang=en
. Et après avoir cliqué sur
le lien, $this->lang
sera à nouveau 'en'
.
Nous recommandons d'indiquer également le type de données pour la propriété (par ex. string
) et vous pouvez
également spécifier une valeur par défaut. Les valeurs des paramètres peuvent être validées.
Les paramètres persistants sont transmis par défaut entre toutes les actions du presenter donné. Pour qu'ils soient également transmis entre plusieurs presenters, il faut les définir soit :
- dans un ancêtre commun dont les presenters héritent
- dans un trait que les presenters utilisent :
trait LanguageAware
{
#[Persistent]
public string $lang;
}
class ProductPresenter extends Nette\Application\UI\Presenter
{
use LanguageAware;
}
Lors de la création d'un lien, la valeur du paramètre persistant peut être modifiée :
<a n:href="Product:show $id, lang: cs">détail en tchèque</a>
Ou il peut être réinitialisé, c'est-à-dire supprimé de l'URL. Il prendra alors sa valeur par défaut :
<a n:href="Product:show $id, lang: null">cliquer</a>
Composants interactifs
Les presenters ont un système de composants intégré. Les composants sont des unités autonomes et réutilisables que nous insérons dans les presenters. Il peut s'agir de formulaires, de datagrids, de menus, en fait de tout ce qu'il est judicieux d'utiliser de manière répétée.
Comment les composants sont-ils insérés dans le presenter et ensuite utilisés ? Vous l'apprendrez dans le chapitre Composants. Vous découvrirez même ce qu'ils ont en commun avec Hollywood.
Et où puis-je obtenir des composants ? Sur la page Componette, vous trouverez des composants open-source ainsi que de nombreux autres add-ons pour Nette, placés ici par des bénévoles de la communauté autour du framework.
Allons plus en profondeur
Ce que nous avons montré jusqu'à présent dans ce chapitre vous suffira probablement amplement. Les lignes suivantes sont destinées à ceux qui s'intéressent aux presenters en profondeur et veulent tout savoir.
Validation des paramètres
Les valeurs des paramètres de la requête et des paramètres persistants reçues de l'URL sont écrites dans les propriétés par la
méthode loadState()
. Celle-ci vérifie également si le type de données indiqué pour la propriété correspond,
sinon elle répond par une erreur 404 et la page ne s'affiche pas.
Ne faites jamais confiance aveuglément aux paramètres, car ils peuvent être facilement modifiés par l'utilisateur dans
l'URL. Voici comment nous vérifions, par exemple, si la langue $this->lang
fait partie des langues prises en
charge. Une bonne approche consiste à redéfinir la méthode loadState()
mentionnée :
class ProductPresenter extends Nette\Application\UI\Presenter
{
#[Persistent]
public string $lang;
public function loadState(array $params): void
{
parent::loadState($params); // ici $this->lang est défini
// suit la vérification personnalisée de la valeur :
if (!in_array($this->lang, ['en', 'cs'])) {
$this->error();
}
}
}
Sauvegarde et restauration de la requête
La requête traitée par le presenter est un objet Nette\Application\Request et est retournée par
la méthode du presenter getRequest()
.
La requête actuelle peut être sauvegardée dans la session ou au contraire restaurée à partir de celle-ci et laisser le
presenter l'exécuter à nouveau. C'est utile par exemple dans une situation où l'utilisateur remplit un formulaire et sa
connexion expire. Pour ne pas perdre les données, avant de rediriger vers la page de connexion, nous sauvegardons la requête
actuelle dans la session à l'aide de $reqId = $this->storeRequest()
, qui retourne son identifiant sous forme de
chaîne courte, et nous le passons comme paramètre au presenter de connexion.
Après la connexion, nous appelons la méthode $this->restoreRequest($reqId)
, qui récupère la requête de la
session et la transmet (forward). La méthode vérifie en même temps que la requête a été créée par le même utilisateur que
celui qui vient de se connecter. Si un autre utilisateur se connecte ou si la clé est invalide, elle ne fait rien et le programme
continue.
Consultez le guide Comment revenir à une page précédente.
Canonisation
Les presenters ont une fonctionnalité vraiment géniale qui contribue à un meilleur SEO (optimisation pour les moteurs de
recherche). Ils empêchent automatiquement l'existence de contenu dupliqué sur différentes URL. Si plusieurs adresses URL
mènent à une certaine cible, par ex. /index
et /index?page=1
, le framework détermine l'une d'elles
comme étant la principale (canonique) et redirige les autres vers elle à l'aide du code HTTP 301. Grâce à cela, les moteurs
de recherche n'indexent pas vos pages deux fois et ne diluent pas leur page rank.
Ce processus est appelé canonisation. L'URL canonique est celle générée par le routeur, généralement la première route correspondante dans la collection.
La canonisation est activée par défaut et peut être désactivée via $this->autoCanonicalize = false
.
La redirection ne se produit pas lors d'une requête AJAX ou POST, car cela entraînerait une perte de données ou n'aurait aucune valeur ajoutée du point de vue du SEO.
Vous pouvez également déclencher la canonisation manuellement à l'aide de la méthode canonicalize()
, à
laquelle, comme à la méthode link()
, sont passés le presenter, l'action et les paramètres. Elle crée un lien et
le compare à l'URL actuelle. S'ils diffèrent, elle redirige vers le lien généré.
public function actionShow(int $id, ?string $slug = null): void
{
$realSlug = $this->facade->getSlugForId($id);
// redirige si $slug diffère de $realSlug
$this->canonicalize('Product:show', [$id, $realSlug]);
}
Événements
En plus des méthodes startup()
, beforeRender()
et shutdown()
, qui sont appelées dans
le cadre du cycle de vie du presenter, il est possible de définir d'autres fonctions qui doivent être appelées automatiquement.
Le presenter définit ce qu'on appelle des événements, dont vous
ajoutez les gestionnaires aux tableaux $onStartup
, $onRender
et $onShutdown
.
class ArticlePresenter extends Nette\Application\UI\Presenter
{
public function __construct()
{
$this->onStartup[] = function () {
// ...
};
}
}
Les gestionnaires dans le tableau $onStartup
sont appelés juste avant la méthode startup()
, ensuite
$onRender
entre beforeRender()
et render<View>()
et enfin $onShutdown
juste avant shutdown()
.
Réponses
La réponse retournée par le presenter est un objet implémentant l'interface Nette\Application\Response. Il existe un certain nombre de réponses prêtes à l'emploi :
- Nette\Application\Responses\CallbackResponse – envoie un callback
- Nette\Application\Responses\FileResponse – envoie un fichier
- Nette\Application\Responses\ForwardResponse – forward()
- Nette\Application\Responses\JsonResponse – envoie du JSON
- Nette\Application\Responses\RedirectResponse – redirection
- Nette\Application\Responses\TextResponse – envoie du texte
- Nette\Application\Responses\VoidResponse – réponse vide
Les réponses sont envoyées avec la méthode sendResponse()
:
use Nette\Application\Responses;
// Texte simple
$this->sendResponse(new Responses\TextResponse('Hello Nette!'));
// Envoie un fichier
$this->sendResponse(new Responses\FileResponse(__DIR__ . '/invoice.pdf', 'Invoice13.pdf'));
// La réponse sera un callback
$callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse) {
if ($httpResponse->getHeader('Content-Type') === 'text/html') {
echo '<h1>Hello</h1>';
}
};
$this->sendResponse(new Responses\CallbackResponse($callback));
Restriction d'accès avec #[Requires]
L'attribut #[Requires]
offre des options avancées pour restreindre l'accès aux presenters et à leurs méthodes.
Il peut être utilisé pour spécifier les méthodes HTTP, exiger une requête AJAX, limiter à la même origine (same origin), et
l'accès uniquement via le forwarding. L'attribut peut être appliqué à la fois aux classes de presenter et aux méthodes
individuelles action<Action>()
, render<View>()
, handle<Signal>()
et
createComponent<Name>()
.
Vous pouvez spécifier ces restrictions :
- sur les méthodes HTTP :
#[Requires(methods: ['GET', 'POST'])]
- exiger une requête AJAX :
#[Requires(ajax: true)]
- accès uniquement depuis la même origine :
#[Requires(sameOrigin: true)]
- accès uniquement via forward :
#[Requires(forward: true)]
- restriction à des actions spécifiques :
#[Requires(actions: 'default')]
Les détails se trouvent dans le guide Comment utiliser l'attribut Requires.
Vérification de la méthode HTTP
Les presenters dans Nette vérifient automatiquement la méthode HTTP de chaque requête entrante. La raison de cette
vérification est principalement la sécurité. Par défaut, les méthodes GET
, POST
, HEAD
,
PUT
, DELETE
, PATCH
sont autorisées.
Si vous souhaitez autoriser en plus, par exemple, la méthode OPTIONS
, utilisez l'attribut
#[Requires]
(depuis Nette Application v3.2) :
#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])]
class MyPresenter extends Nette\Application\UI\Presenter
{
}
Dans la version 3.1, la vérification est effectuée dans checkHttpMethod()
, qui vérifie si la méthode
spécifiée dans la requête est contenue dans le tableau $presenter->allowedMethods
. L'ajout de la méthode se
fait comme ceci :
class MyPresenter extends Nette\Application\UI\Presenter
{
protected function checkHttpMethod(): void
{
$this->allowedMethods[] = 'OPTIONS';
parent::checkHttpMethod();
}
}
Il est important de souligner que si vous autorisez la méthode OPTIONS
, vous devez ensuite la gérer correctement
dans votre presenter. La méthode est souvent utilisée comme une requête dite preflight, que le navigateur envoie
automatiquement avant la requête réelle lorsqu'il est nécessaire de déterminer si la requête est autorisée du point de vue
de la politique CORS (Cross-Origin Resource Sharing). Si vous autorisez la méthode mais n'implémentez pas la réponse correcte,
cela peut entraîner des incohérences et des problèmes de sécurité potentiels.