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.

Cycle de vie du presenter

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 :

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 :

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.

Lectures complémentaires

version: 4.0