Composants interactifs

Les composants sont des objets réutilisables indépendants que nous insérons dans les pages. Il peut s'agir de formulaires, de datagrids, de sondages, en fait de tout ce qu'il est judicieux d'utiliser de manière répétée. Nous allons montrer :

  • comment utiliser les composants ?
  • comment les écrire ?
  • que sont les signaux ?

Nette intègre un système de composants. Les vétérans peuvent se souvenir de quelque chose de similaire dans Delphi ou ASP.NET Web Forms ; React ou Vue.js sont basés sur quelque chose de vaguement similaire. Cependant, dans le monde des frameworks PHP, c'est une caractéristique unique.

Pourtant, les composants influencent fondamentalement l'approche de la création d'applications. Vous pouvez composer des pages à partir d'unités préfabriquées. Besoin d'un datagrid dans l'administration ? Vous le trouverez sur Componette, un dépôt d'add-ons open-source (donc pas seulement des composants) pour Nette, et l'insérerez simplement dans le presenter.

Vous pouvez intégrer n'importe quel nombre de composants dans un presenter. Et vous pouvez insérer d'autres composants dans certains composants. Cela crée un arbre de composants, dont la racine est le presenter.

Méthodes Factory

Comment les composants sont-ils insérés dans le presenter et ensuite utilisés ? Généralement via des méthodes factory.

Une factory de composants représente une manière élégante de créer des composants uniquement lorsqu'ils sont réellement nécessaires (lazy / on demand). Toute la magie réside dans l'implémentation d'une méthode nommée createComponent<Name>(), où <Name> est le nom du composant à créer, et qui crée et retourne le composant.

class DefaultPresenter extends Nette\Application\UI\Presenter
{
	protected function createComponentPoll(): PollControl
	{
		$poll = new PollControl;
		$poll->items = $this->item;
		return $poll;
	}
}

Grâce au fait que tous les composants sont créés dans des méthodes séparées, le code gagne en clarté.

Les noms des composants commencent toujours par une lettre minuscule, même s'ils sont écrits avec une majuscule dans le nom de la méthode.

Nous n'appelons jamais les factories directement ; elles s'appellent elles-mêmes la première fois que nous utilisons le composant. Grâce à cela, le composant est créé au bon moment et seulement s'il est réellement nécessaire. Si nous n'utilisons pas le composant (par exemple, lors d'une requête AJAX où seule une partie de la page est transmise, ou lors de la mise en cache du template), il n'est pas créé du tout et nous économisons les performances du serveur.

// nous accédons au composant et si c'est la première fois,
// createComponentPoll() est appelée, qui le crée
$poll = $this->getComponent('poll');
// syntaxe alternative : $poll = $this['poll'];

Dans le template, il est possible de rendre un composant à l'aide de la balise {control}. Il n'est donc pas nécessaire de transmettre manuellement les composants au template.

<h2>Votez</h2>

{control poll}

Style Hollywood

Les composants utilisent couramment une technique fraîche que nous aimons appeler le style Hollywood. Vous connaissez sûrement la phrase célèbre que les participants aux auditions de films entendent si souvent : “Ne nous appelez pas, nous vous appellerons”. Et c'est exactement de cela qu'il s'agit.

Dans Nette, au lieu de devoir constamment demander (“le formulaire a-t-il été soumis ?”, “était-il valide ?” ou “l'utilisateur a-t-il appuyé sur ce bouton ?”), vous dites au framework “quand cela arrivera, appelle cette méthode” et vous lui laissez le reste du travail. Si vous programmez en JavaScript, vous connaissez bien ce style de programmation. Vous écrivez des fonctions qui sont appelées lorsqu'un certain événement se produit. Et le langage leur transmet les paramètres appropriés.

Cela change complètement la façon d'écrire des applications. Plus vous pouvez laisser de tâches au framework, moins vous avez de travail. Et moins vous risquez d'oublier quelque chose.

Écrire un composant

Par le terme composant, nous entendons généralement un descendant de la classe Nette\Application\UI\Control. (Il serait donc plus précis d'utiliser le terme “controls”, mais “contrôles” a un sens différent en français et “composants” s'est plutôt imposé.) Le presenter lui-même Nette\Application\UI\Presenter est d'ailleurs aussi un descendant de la classe Control.

use Nette\Application\UI\Control;

class PollControl extends Control
{
}

Rendu

Nous savons déjà que pour rendre un composant, on utilise la balise {control componentName}. Celle-ci appelle en fait la méthode render() du composant, dans laquelle nous nous occupons du rendu. Nous avons à notre disposition, tout comme dans le presenter, un template Latte dans la variable $this->template, auquel nous passons des paramètres. Contrairement au presenter, nous devons spécifier le fichier de template et le faire rendre :

public function render(): void
{
	// nous insérons quelques paramètres dans le template
	$this->template->param = $value;
	// et nous le rendons
	$this->template->render(__DIR__ . '/poll.latte');
}

La balise {control} permet de passer des paramètres à la méthode render() :

{control poll $id, $message}
public function render(int $id, string $message): void
{
	// ...
}

Parfois, un composant peut être constitué de plusieurs parties que nous voulons rendre séparément. Pour chacune d'elles, nous créons notre propre méthode de rendu, ici dans l'exemple renderPaginator() :

public function renderPaginator(): void
{
	// ...
}

Et dans le template, nous l'appelons ensuite en utilisant :

{control poll:paginator}

Pour une meilleure compréhension, il est bon de savoir comment cette balise est traduite en PHP.

{control poll}
{control poll:paginator 123, 'hello'}

se traduit par :

$control->getComponent('poll')->render();
$control->getComponent('poll')->renderPaginator(123, 'hello');

La méthode getComponent() retourne le composant poll et appelle la méthode render() sur ce composant, ou renderPaginator() si un autre mode de rendu est spécifié dans la balise après les deux-points.

Attention, si => apparaît n'importe où dans les paramètres, tous les paramètres seront enveloppés dans un tableau et passés comme premier argument :

{control poll, id: 123, message: 'hello'}

se traduit par :

$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']);

Rendu d'un sous-composant :

{control cartControl-someForm}

se traduit par :

$control->getComponent("cartControl-someForm")->render();

Les composants, tout comme les presenters, transmettent automatiquement plusieurs variables utiles aux templates :

  • $basePath est le chemin URL absolu vers le répertoire racine (par ex. /eshop)
  • $baseUrl est l'URL absolue vers le répertoire racine (par ex. http://localhost/eshop)
  • $user est l'objet représentant l'utilisateur
  • $presenter est le presenter actuel
  • $control est le composant actuel
  • $flashes tableau des messages envoyés par la fonction flashMessage()

Signal

Nous savons déjà que la navigation dans une application Nette consiste à créer des liens ou des redirections vers des paires Presenter:action. Mais que faire si nous voulons simplement effectuer une action sur la page actuelle ? Par exemple, changer le tri des colonnes dans un tableau ; supprimer un élément ; basculer entre le mode clair/sombre ; soumettre un formulaire ; voter dans un sondage ; etc.

Ce type de requête est appelé signal. Et tout comme les actions appellent les méthodes action<Action>() ou render<Action>(), les signaux appellent les méthodes handle<Signal>(). Alors que le concept d'action (ou de vue) est purement lié aux presenters, les signaux concernent tous les composants. Et donc aussi les presenters, car UI\Presenter est un descendant de UI\Control.

public function handleClick(int $x, int $y): void
{
	// ... traitement du signal ...
}

Nous créons un lien qui appelle un signal de la manière habituelle, c'est-à-dire dans le template avec l'attribut n:href ou la balise {link}, dans le code avec la méthode link(). Plus d'informations dans le chapitre Création de liens URL.

<a n:href="click! $x, $y">cliquez ici</a>

Un signal est toujours appelé sur le presenter et l'action actuels ; il n'est pas possible de l'appeler sur un autre presenter ou une autre action.

Le signal provoque donc un rechargement de la page exactement comme lors de la requête initiale, mais appelle en plus la méthode de gestion du signal avec les paramètres appropriés. Si la méthode n'existe pas, une exception Nette\Application\UI\BadSignalException est levée, qui s'affiche à l'utilisateur comme une page d'erreur 403 Interdit.

Snippets et AJAX

Les signaux vous rappellent peut-être un peu AJAX : des gestionnaires qui sont appelés sur la page actuelle. Et vous avez raison, les signaux sont en effet souvent appelés via AJAX, puis seules les parties modifiées de la page sont transmises au navigateur. C'est-à-dire les fameux snippets. Plus d'informations peuvent être trouvées sur la page dédiée à AJAX.

Messages Flash

Un composant possède son propre stockage de messages flash indépendant du presenter. Ce sont des messages qui informent par exemple 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.

L'envoi est géré par la méthode flashMessage. Le premier paramètre est le texte du message ou un objet stdClass représentant le message. Le deuxième paramètre facultatif est son type (erreur, avertissement, info, etc.). La méthode flashMessage() retourne une instance du message flash sous forme d'objet stdClass, auquel 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}

Redirection après un signal

Le traitement d'un signal de composant est souvent suivi d'une redirection. C'est une situation similaire à celle des formulaires – après leur soumission, nous redirigeons également pour éviter que les données ne soient renvoyées si la page est rafraîchie dans le navigateur.

$this->redirect('this') // redirige vers le presenter et l'action actuels

Comme un composant est un élément réutilisable et ne devrait généralement pas avoir de lien direct avec des presenters spécifiques, les méthodes redirect() et link() interprètent automatiquement le paramètre comme un signal du composant :

$this->redirect('click') // redirige vers le signal 'click' du même composant

Si vous avez besoin de rediriger vers un autre presenter ou une autre action, vous pouvez le faire via le presenter :

$this->getPresenter()->redirect('Product:show'); // redirige vers un autre presenter/action

Paramètres persistants

Les paramètres persistants sont utilisés pour maintenir l'état dans les composants 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, y compris pour les liens créés dans d'autres composants sur la même page.

Vous avez par exemple un composant pour la pagination du contenu. Il peut y avoir plusieurs de ces composants sur une page. Et nous souhaitons qu'après avoir cliqué sur un lien, tous les composants restent sur leur page actuelle. C'est pourquoi nous faisons du numéro de page (page) un paramètre persistant.

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 PaginatingControl extends Control
{
	#[Persistent]
	public int $page = 1; // doit être public
}

Nous recommandons d'indiquer également le type de données pour la propriété (par ex. int) et vous pouvez également spécifier une valeur par défaut. Les valeurs des paramètres peuvent être validées.

Lors de la création d'un lien, la valeur du paramètre persistant peut être modifiée :

<a n:href="this page: $page + 1">suivant</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="this page: null">réinitialiser</a>

Composants persistants

Non seulement les paramètres, mais aussi les composants peuvent être persistants. Pour un tel composant, ses paramètres persistants sont également transmis entre différentes actions du presenter ou entre plusieurs presenters. Nous marquons les composants persistants avec une annotation dans la classe du presenter. Par exemple, nous marquons ainsi les composants calendar et poll :

/**
 * @persistent(calendar, poll)
 */
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}

Il n'est pas nécessaire de marquer les sous-composants à l'intérieur de ces composants ; ils deviendront également persistants.

En PHP 8, vous pouvez également utiliser des attributs pour marquer les composants persistants :

use Nette\Application\Attributes\Persistent;

#[Persistent('calendar', 'poll')]
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}

Composants avec dépendances

Comment créer des composants avec des dépendances sans “polluer” les presenters qui les utiliseront ? Grâce aux fonctionnalités intelligentes du conteneur DI de Nette, comme pour l'utilisation de services classiques, la majeure partie du travail peut être laissée au framework.

Prenons comme exemple un composant qui a une dépendance envers le service PollFacade :

class PollControl extends Control
{
	public function __construct(
		private int $id, //  Id du sondage pour lequel nous créons le composant
		private PollFacade $facade,
	) {
	}

	public function handleVote(int $voteId): void
	{
		$this->facade->vote($this->id, $voteId);
		// ...
	}
}

Si nous écrivions un service classique, il n'y aurait rien à faire. Le conteneur DI se chargerait invisiblement de transmettre toutes les dépendances. Mais avec les composants, nous les traitons généralement en créant leur nouvelle instance directement dans le presenter dans les méthodes factory createComponent…(). Mais transmettre toutes les dépendances de tous les composants au presenter pour ensuite les transmettre aux composants est lourd. Et tout ce code écrit…

La question logique est, pourquoi ne pas simplement enregistrer le composant comme un service classique, le passer au presenter et ensuite le retourner dans la méthode createComponent…() ? Une telle approche est cependant inappropriée, car nous voulons pouvoir créer le composant plusieurs fois si nécessaire.

La solution correcte est d'écrire une factory pour le composant, c'est-à-dire une classe qui nous créera le composant :

class PollControlFactory
{
	public function __construct(
		private PollFacade $facade,
	) {
	}

	public function create(int $id): PollControl
	{
		return new PollControl($id, $this->facade);
	}
}

Nous enregistrons cette factory dans notre conteneur dans la configuration :

services:
	- PollControlFactory

et enfin, nous l'utilisons dans notre presenter :

class PollPresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private PollControlFactory $pollControlFactory,
	) {
	}

	protected function createComponentPollControl(): PollControl
	{
		$pollId = 1; // nous pouvons passer notre paramètre
		return $this->pollControlFactory->create($pollId);
	}
}

Ce qui est génial, c'est que Nette DI peut générer de telles factories simples, donc au lieu de tout son code, il suffit d'écrire seulement son interface :

interface PollControlFactory
{
	public function create(int $id): PollControl;
}

Et c'est tout. Nette implémente intérieurement cette interface et la transmet au presenter, où nous pouvons déjà l'utiliser. Il ajoute magiquement le paramètre $id et l'instance de la classe PollFacade à notre composant.

Composants en profondeur

Les composants dans Nette Application représentent des parties réutilisables d'une application web que nous insérons dans les pages et auxquelles ce chapitre entier est d'ailleurs consacré. Quelles sont exactement les capacités d'un tel composant ?

  1. il est rendable dans un template
  2. il sait quelle partie de lui-même rendre lors d'une requête AJAX (snippets)
  3. il a la capacité de sauvegarder son état dans l'URL (paramètres persistants)
  4. il a la capacité de réagir aux actions de l'utilisateur (signaux)
  5. il crée une structure hiérarchique (où la racine est le presenter)

Chacune de ces fonctions est assurée par l'une des classes de la lignée d'héritage. Le rendu (1 + 2) est géré par Nette\Application\UI\Control, l'intégration dans le cycle de vie (3, 4) par la classe Nette\Application\UI\Component et la création d'une structure hiérarchique (5) par les classes Container et Component.

Nette\ComponentModel\Component  { IComponent }
|
+- Nette\ComponentModel\Container  { IContainer }
	|
	+- Nette\Application\UI\Component  { SignalReceiver, StatePersistent }
		|
		+- Nette\Application\UI\Control  { Renderable }
			|
			+- Nette\Application\UI\Presenter  { IPresenter }

Cycle de vie du composant

Cycle de vie du composant

Validation des paramètres persistants

Les valeurs 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 persistants, car ils peuvent être facilement modifiés par l'utilisateur dans l'URL. Voici comment nous vérifions, par exemple, si le numéro de page $this->page est supérieur à 0. Une bonne approche consiste à redéfinir la méthode loadState() mentionnée :

class PaginatingControl extends Control
{
	#[Persistent]
	public int $page = 1;

	public function loadState(array $params): void
	{
		parent::loadState($params); // ici $this->page est défini
		// suit la vérification personnalisée de la valeur :
		if ($this->page < 1) {
			$this->error();
		}
	}
}

Le processus inverse, c'est-à-dire la collecte des valeurs des propriétés persistantes, est géré par la méthode saveState().

Signaux en profondeur

Un signal provoque un rechargement de la page exactement comme lors de la requête initiale (sauf s'il est appelé via AJAX) et appelle la méthode signalReceived($signal), dont l'implémentation par défaut dans la classe Nette\Application\UI\Component tente d'appeler une méthode composée des mots handle{signal}. Le traitement ultérieur dépend de l'objet donné. Les objets qui héritent de Component (c'est-à-dire Control et Presenter) réagissent en essayant d'appeler la méthode handle{signal} avec les paramètres appropriés.

En d'autres termes : la définition de la fonction handle{signal} est prise, ainsi que tous les paramètres qui sont arrivés avec la requête, et les paramètres de l'URL sont substitués aux arguments par nom, puis on tente d'appeler la méthode donnée. Par exemple, la valeur du paramètre id dans l'URL est passée comme paramètre $id, something de l'URL est passé comme $something, etc. Et si la méthode n'existe pas, la méthode signalReceived lève une exception.

Un signal peut être reçu par n'importe quel composant, presenter ou objet qui implémente l'interface SignalReceiver et est connecté à l'arbre des composants.

Les principaux destinataires des signaux seront les Presenters et les composants visuels héritant de Control. Un signal doit servir de signe à un objet qu'il doit faire quelque chose – un sondage doit compter un vote d'un utilisateur, un bloc d'actualités doit se déplier et afficher deux fois plus d'actualités, un formulaire a été soumis et doit traiter les données, etc.

Nous créons l'URL pour un signal à l'aide de la méthode Component::link(). Comme paramètre $destination, nous passons la chaîne {signal}! et comme $args, un tableau d'arguments que nous voulons passer au signal. Le signal est toujours appelé sur le presenter et l'action actuels avec les paramètres actuels ; les paramètres du signal sont simplement ajoutés. De plus, le paramètre ?do, qui spécifie le signal, est ajouté au début.

Son format est soit {signal}, soit {signalReceiver}-{signal}. {signalReceiver} est le nom du composant dans le presenter. C'est pourquoi un trait d'union ne peut pas être utilisé dans le nom d'un composant – il est utilisé pour séparer le nom du composant et le signal, mais il est possible d'imbriquer plusieurs composants de cette manière.

La méthode isSignalReceiver() vérifie si le composant (premier argument) est le destinataire du signal (deuxième argument). Nous pouvons omettre le deuxième argument – il vérifie alors si le composant est le destinataire de n'importe quel signal. On peut passer true comme deuxième paramètre pour vérifier si non seulement le composant spécifié est le destinataire, mais aussi n'importe lequel de ses descendants.

À n'importe quelle étape précédant handle{signal}, nous pouvons exécuter le signal manuellement en appelant la méthode processSignal(), qui se charge de traiter le signal – elle prend le composant désigné comme destinataire du signal (s'il n'y a pas de destinataire spécifié, c'est le presenter lui-même) et lui envoie le signal.

Exemple :

if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) {
	$this->processSignal();
}

Le signal est ainsi exécuté prématurément et ne sera plus appelé à nouveau.

version: 4.0