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 fonctionflashMessage()
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 ?
- il est rendable dans un template
- il sait quelle partie de lui-même rendre lors d'une requête AJAX (snippets)
- il a la capacité de sauvegarder son état dans l'URL (paramètres persistants)
- il a la capacité de réagir aux actions de l'utilisateur (signaux)
- 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
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.