Composants interactifs

Les composants sont des objets distincts réutilisables que nous plaçons dans les pages. Il peut s'agir de formulaires, de grilles de données, de sondages, en fait de tout ce qui peut être utilisé de manière répétée. Nous allons montrer :

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

Nette dispose d'un système de composants intégré. Les plus anciens d'entre vous se souviennent peut-être de quelque chose de similaire dans Delphi ou ASP.NET Web Forms. React ou Vue.js sont construits sur quelque chose de vaguement similaire. Cependant, dans le monde des frameworks PHP, il s'agit d'une fonctionnalité tout à fait unique.

Dans le même temps, les composants changent fondamentalement l'approche du développement d'applications. Vous pouvez composer des pages à partir d'unités préparées à l'avance. Vous avez besoin d'un datagrid dans l'administration ? Vous pouvez la trouver dans Componette, un référentiel de modules complémentaires (et pas seulement de composants) open-source pour Nette, et la coller simplement dans le présentateur.

Vous pouvez incorporer n'importe quel nombre de composants dans le présentateur. Et vous pouvez insérer d'autres composants dans certains composants. Cela crée un arbre de composants avec un présentateur comme racine.

Méthodes d'usine

Comment les composants sont-ils placés et ensuite utilisés dans le présentateur ? Généralement à l'aide de méthodes d'usine.

La fabrique de composants est un moyen élégant de créer des composants uniquement lorsqu'ils sont vraiment nécessaires (paresseux / à la demande). Toute la magie réside dans la mise en œuvre d'une méthode appelée createComponent<Name>()<Name> est le nom du composant, qui sera créé et retourné.

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

Comme tous les composants sont créés dans des méthodes distinctes, le code est plus propre et plus facile à lire.

Les noms des composants commencent toujours par une lettre minuscule, bien qu'ils soient en majuscules dans le nom de la méthode.

Nous n'appelons jamais les fabriques directement, elles sont appelées automatiquement, lorsque nous utilisons des composants pour la première fois. Grâce à cela, un composant est créé au bon moment, et seulement s'il est vraiment nécessaire. Si nous n'utilisons pas le composant (par exemple sur une requête AJAX, où nous ne retournons qu'une partie de la page, ou lorsque des parties sont mises en cache), il ne sera même pas créé et nous économisons les performances du serveur.

// on accède au composant et si c'est la première fois,
// on appelle createComponentPoll() pour le créer
$poll = $this->getComponent('poll');
// syntaxe alternative: $poll = $this['poll'];

Dans le modèle, vous pouvez rendre un composant en utilisant la balise {control}. Il n'est donc pas nécessaire de passer manuellement les composants au modèle.

<h2>Please Vote</h2>

{control poll}

Le style Hollywood

Les composants utilisent couramment une technique cool, que nous aimons appeler le style Hollywood. Vous connaissez certainement le cliché que les acteurs entendent souvent lors des appels de casting : “Ne nous appelez pas, nous vous appellerons.” Et c'est bien de cela qu'il s'agit.

Dans Nette, au lieu de devoir constamment poser des questions (“le formulaire a-t-il été soumis ?”, “était-il valide ?” ou “quelqu'un a-t-il appuyé sur ce bouton ?”), vous dites au framework “lorsque ceci se produit, appelez cette méthode” et vous lui laissez la suite du travail. Si vous programmez en JavaScript, vous êtes familier avec ce style de programmation. Vous écrivez des fonctions qui sont appelées lorsqu'un certain événement se produit. Et le moteur leur passe les paramètres appropriés.

Cela change complètement la façon dont vous écrivez des applications. Plus vous pouvez déléguer de tâches au framework, moins vous avez de travail. Et moins vous pouvez oublier.

Comment écrire un composant

Par composant, nous entendons généralement les descendants de la classe Nette\Application\UI\Control. Le présentateur Nette\Application\UI\Presenter lui-même est également un descendant de la classe Control.

use Nette\Application\UI\Control;

class PollControl extends Control
{
}

Rendu

Nous savons déjà que la balise {control componentName} est utilisée pour dessiner un composant. Elle appelle en fait la méthode render() du composant, dans laquelle nous nous occupons du rendu. Nous disposons, comme dans le présentateur, d'un modèle Latte dans la variable $this->template, à laquelle nous passons les paramètres. Contrairement à l'utilisation dans un présentateur, nous devons spécifier un fichier de modèle et le laisser effectuer le rendu :

public function render(): void
{
	// nous allons mettre quelques paramètres dans le modèle
	$this->template->param = $value;
	// et le dessiner
	$this->template->render(__DIR__ . '/poll.latte');
}

Le tag {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 composé de plusieurs parties que nous voulons rendre séparément. Pour chacune d'entre elles, nous allons créer notre propre méthode de rendu, voici par exemple renderPaginator():

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

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

{control poll:paginator}

Pour une meilleure compréhension, il est bon de savoir comment la balise est compilée en code PHP.

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

Cela se compile en :

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

getComponent() La méthode renvoie le composant poll, puis la méthode render() ou renderPaginator(), respectivement, est appelée sur celui-ci.

Si n'importe où dans la partie paramètre => est utilisé, tous les paramètres seront enveloppés dans un tableau et passés comme premier argument :

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

compile vers :

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

Rendu du sous-composant :

{control cartControl-someForm}

compile vers :

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

Les composants, comme les présentateurs, transmettent automatiquement plusieurs variables utiles aux modèles :

  • $basePath est un chemin URL absolu vers le répertoire racine (par exemple /CD-collection)
  • $baseUrl est une URL absolue vers le répertoire racine (par exemple http://localhost/CD-collection)
  • $user est un objet représentant l'utilisateur
  • $presenter est le présentateur actuel
  • $control est le composant actuel
  • $flashes liste des messages envoyés par la méthode flashMessage()

Signal

Nous savons déjà que la navigation dans l'application Nette consiste à créer des liens ou à rediriger vers des paires Presenter:action. Mais qu'en est-il si nous voulons simplement effectuer une action sur la page courante ? Par exemple, changer l'ordre de tri de la colonne dans le tableau ; supprimer un élément ; passer en mode lumière/obscurité ; soumettre le formulaire ; voter dans le sondage ; etc.

Ce type de demande s'appelle un signal. Et comme les actions invoquent des méthodes action<Action>() ou render<Action>()les signaux appellent des méthodes handle<Signal>(). Alors que le concept d'action (ou de vue) ne concerne que les présentateurs, les signaux s'appliquent à tous les composants. Et donc aussi aux présentateurs, car UI\Presenter est un descendant de UI\Control.

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

Le lien qui appelle le signal est créé de la manière habituelle, c'est-à-dire dans le modèle par l'attribut n:href ou la balise {link}, dans le code par la méthode link(). Pour en savoir plus, consultez le chapitre Création de liens URL.

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

Le signal est toujours appelé dans le présentateur et la vue actuels, il n'est donc pas possible d'établir un lien vers le signal dans un présentateur/une action différents.

Ainsi, le signal provoque le rechargement de la page exactement de la même manière que dans la requête originale, seulement en plus il appelle la méthode de traitement du signal avec les paramètres appropriés. Si la méthode n'existe pas, l'exception Nette\Application\UI\BadSignalException est levée, ce qui est affiché à l'utilisateur comme page d'erreur 403 Forbidden.

Snippets et AJAX

Les signaux peuvent vous rappeler un peu AJAX : des gestionnaires qui sont appelés sur la page actuelle. Et vous avez raison, les signaux sont très souvent appelés en utilisant AJAX, et ensuite nous ne transmettons au navigateur que les parties modifiées de la page. On les appelle des snippets. Vous trouverez plus d'informations sur la page concernant AJAX.

Messages Flash

Un composant dispose de son propre stockage de messages flash, indépendamment du présentateur. Il s'agit de messages qui, par exemple, informent sur le résultat de l'opération. Une caractéristique importante des messages flash est qu'ils sont disponibles dans le modèle même après une redirection. Même après avoir été affichés, ils restent vivants pendant 30 secondes supplémentaires – par exemple, au cas où l'utilisateur rafraîchirait involontairement la page – le message ne sera pas perdu.

L'envoi se fait par la méthode flashMessage. Le premier paramètre est le texte du message ou l'objet stdClass représentant le message. Le deuxième paramètre facultatif est son type (erreur, avertissement, info, etc.). La méthode flashMessage() renvoie une instance de flashMessage sous forme d'objet stdClass auquel vous pouvez passer des informations.

$this->flashMessage('L'article a été supprimé.');
$this->redirect(/* ... */); // et redirection

Dans le modèle, 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 déjà mentionnées. Nous les dessinons comme suit :

{foreach $flashes as $flash}
	<div class="flash {$flash->type}">{$flash->message}</div>
{/foreach}

Paramètres persistants

Les paramètres persistants sont utilisés pour maintenir l'état des composants entre les différentes requêtes. Leur valeur reste inchangée même après avoir cliqué sur un lien. Contrairement aux données de session, ils sont transférés dans l'URL. Ils sont également transférés automatiquement, y compris les liens créés dans d'autres composants de la même page.

Par exemple, vous avez un composant de pagination de contenu. Il peut y avoir plusieurs composants de ce type sur une page. Vous souhaitez que tous les composants restent sur leur page actuelle lorsque vous cliquez sur le lien. 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 baliser avec l'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 publique
}

Nous vous recommandons d'inclure le type de données (par exemple int) avec la propriété, et vous pouvez également inclure une valeur par défaut. Les valeurs des paramètres peuvent être validées.

Vous pouvez modifier la valeur d'un paramètre persistant lors de la création d'un lien :

<a n:href="this page: $page + 1">next</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">reset</a>

Composants persistants

Non seulement les paramètres mais aussi les composants peuvent être persistants. Leurs paramètres persistants sont également transférés entre différentes actions ou entre différents présentateurs. Nous marquons les composants persistants avec ces annotations pour la classe de présentateur. Par exemple, nous marquons ici les composants calendar et poll comme suit :

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

Il n'est pas nécessaire de marquer les sous-composants comme persistants, ils le sont automatiquement.

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 “bousiller” les présentateurs qui les utiliseront ? Grâce aux fonctionnalités astucieuses du conteneur DI de Nette, comme pour l'utilisation de services traditionnels, nous pouvons laisser la majeure partie du travail au framework.

Prenons l'exemple d'un composant qui a une dépendance avec le service PollFacade:

class PollControl extends Control
{
	public function __construct(
		private int $id, // Id d'un sondage, pour lequel le composant est créé
		private PollFacade $facade,
	) {
	}

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

Si nous écrivions un service classique, il n'y aurait pas à s'inquiéter. Le conteneur DI se chargerait invisiblement de passer toutes les dépendances. Mais nous gérons généralement les composants en créant une nouvelle instance de ceux-ci directement dans le présentateur à l'aide de méthodes d'usine createComponent...(). Mais transmettre toutes les dépendances de tous les composants au présentateur pour ensuite les transmettre aux composants est fastidieux. Et la quantité de code écrite…

La question logique est la suivante : pourquoi ne pas simplement enregistrer le composant en tant que service classique, le transmettre au présentateur, puis le retourner dans la méthode createComponent...()? Mais cette approche est inappropriée car nous voulons pouvoir créer le composant plusieurs fois.

La bonne solution consiste à écrire une fabrique pour le composant, c'est-à-dire une classe qui crée le composant pour nous :

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

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

Maintenant nous enregistrons notre service à la configuration du conteneur DI :

services:
	- PollControlFactory

Enfin, nous allons utiliser cette fabrique dans notre présentateur :

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 des fabriques aussi simples, donc au lieu d'écrire tout le code, il suffit d'écrire son interface :

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

C'est tout. Nette implémente cette interface en interne et l'injecte dans notre présentateur, où nous pouvons l'utiliser. Elle transmet également comme par magie notre paramètre $id et notre instance de la classe PollFacade à notre composant.

Les composants en profondeur

Les composants d'une application Nette sont les parties réutilisables d'une application Web que nous intégrons dans les pages, ce qui est le sujet de ce chapitre. Quelles sont exactement les capacités d'un tel composant ?

  1. il peut être rendu dans un modèle
  2. il sait quelle partie de lui-même rendre lors d'une requête AJAX (snippets)
  3. il a la capacité de stocker son état dans une URL (paramètres persistants)
  4. il a la capacité de répondre aux actions de l'utilisateur (signaux)
  5. il crée une structure hiérarchique (dont la racine est le présentateur).

Chacune de ces fonctions est géré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'incorporation dans le cycle de vie (3, 4) par la classe Nette\Application\UI\Component et la création de la 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çus des URL sont écrites dans les propriétés par la méthode loadState(). Elle vérifie également si le type de données spécifié pour la propriété correspond, sinon elle répondra par une erreur 404 et la page ne sera pas affichée.

Il ne faut jamais faire aveuglément confiance aux paramètres persistants, car ils peuvent facilement être remplacés par l'utilisateur dans l'URL. Par exemple, voici comment nous vérifions si le numéro de page $this->page est supérieur à 0. Une bonne façon de le faire est de surcharger la méthode loadState() mentionnée ci-dessus :

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

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

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

Signaux en profondeur

Un signal provoque un rechargement de la page comme la requête originale (à l'exception d'AJAX) et invoque 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}. La suite du traitement repose sur l'objet donné. Les objets qui sont des descendants de Component (c'est-à-dire Control et Presenter) tentent d'appeler handle{Signal} avec les paramètres appropriés.

En d'autres termes, la définition de la méthode handle{Signal} est reprise et tous les paramètres reçus dans la demande sont mis en correspondance avec les paramètres de la méthode. Cela signifie que le paramètre id de l'URL correspond au paramètre de la méthode $id, something à $something et ainsi de suite. Et si la méthode n'existe pas, la méthode signalReceived lève une exception.

Le signal peut être reçu par n'importe quel composant, présentateur d'objet qui implémente l'interface SignalReceiver s'il est connecté à l'arbre des composants.

Les principaux récepteurs de signaux sont Presenters et les composants visuels étendant Control. Un signal est un signe pour un objet qu'il doit faire quelque chose – un sondage compte dans un vote de l'utilisateur, la boîte avec des nouvelles doit se déplier, le formulaire a été envoyé et doit traiter des données et ainsi de suite.

L'URL pour le signal est créé en utilisant 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 gestionnaire du signal. Les paramètres du signal sont attachés à l'URL du présentateur/de la vue en cours. Le paramètre ?do dans l'URL détermine le signal appelé.

Son format est {signal} ou {signalReceiver}-{signal}. {signalReceiver} est le nom du composant dans le présentateur. C'est pourquoi le tiret ne peut pas être présent dans le nom des composants – il est utilisé pour diviser le nom du composant et du signal, mais il est possible de composer plusieurs composants.

La méthode isSignalReceiver() vérifie si un composant (premier argument) est un récepteur d'un signal (deuxième argument). Le deuxième argument peut être omis – dans ce cas, la méthode vérifie si le composant est un récepteur d'un signal quelconque. Si le deuxième paramètre est true, elle vérifie si le composant ou ses descendants sont des récepteurs d'un signal.

Dans toute phase précédant handle{Signal}, le signal peut être exécuté manuellement en appelant la méthode processSignal() qui prend en charge l'exécution du signal. Elle prend le composant récepteur (s'il n'est pas défini, c'est le présentateur lui-même) et lui envoie le signal.

Exemple :

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

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

version: 4.0