Réutilisation des formulaires à plusieurs endroits

Dans Nette, vous disposez de plusieurs options pour utiliser le même formulaire à plusieurs endroits sans dupliquer de code. Dans cet article, nous allons examiner différentes solutions, y compris celles que vous devriez éviter.

Factory de formulaires

L'une des approches fondamentales pour utiliser le même composant à plusieurs endroits est de créer une méthode ou une classe qui génère ce composant, puis d'appeler cette méthode à différents endroits de l'application. Une telle méthode ou classe est appelée une factory. Ne confondez pas s'il vous plaît avec le patron de conception factory method, qui décrit une manière spécifique d'utiliser les factories et n'est pas lié à ce sujet.

À titre d'exemple, créons une factory qui assemblera un formulaire d'édition :

use Nette\Application\UI\Form;

class FormFactory
{
	public function createEditForm(): Form
	{
		$form = new Form;
		$form->addText('title', 'Titre :');
		// ici, on ajoute d'autres champs de formulaire
		$form->addSubmit('send', 'Envoyer');
		return $form;
	}
}

Vous pouvez maintenant utiliser cette factory à différents endroits de votre application, par exemple dans les presenters ou les composants. Et ce, en la demandant comme dépendance. Tout d'abord, inscrivons la classe dans le fichier de configuration :

services:
	- FormFactory

Et ensuite, utilisons-la dans un presenter :

class MyPresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private FormFactory $formFactory,
	) {
	}

	protected function createComponentEditForm(): Form
	{
		$form = $this->formFactory->createEditForm();
		$form->onSuccess[] = function () {
			// traitement des données soumises
		};
		return $form;
	}
}

Vous pouvez étendre la factory de formulaires avec d'autres méthodes pour créer d'autres types de formulaires selon les besoins de votre application. Et bien sûr, nous pouvons également ajouter une méthode qui créera un formulaire de base sans éléments, et que les autres méthodes utiliseront :

class FormFactory
{
	public function createForm(): Form
	{
		$form = new Form;
		return $form;
	}

	public function createEditForm(): Form
	{
		$form = $this->createForm();
		$form->addText('title', 'Titre :');
		// ici, on ajoute d'autres champs de formulaire
		$form->addSubmit('send', 'Envoyer');
		return $form;
	}
}

La méthode createForm() ne fait rien d'utile pour le moment, mais cela changera rapidement.

Dépendances de la factory

Avec le temps, il s'avérera que nous avons besoin que les formulaires soient multilingues. Cela signifie que nous devons définir un traducteur pour tous les formulaires. À cette fin, nous modifierons la classe FormFactory pour qu'elle accepte un objet Translator comme dépendance dans le constructeur, et nous le transmettrons au formulaire :

use Nette\Localization\Translator;

class FormFactory
{
	public function __construct(
		private Translator $translator,
	) {
	}

	public function createForm(): Form
	{
		$form = new Form;
		$form->setTranslator($this->translator);
		return $form;
	}

	// ...
}

Comme la méthode createForm() est également appelée par les autres méthodes créant des formulaires spécifiques, il suffit de définir le traducteur uniquement dans celle-ci. Et c'est fait. Il n'est pas nécessaire de modifier le code d'aucun presenter ou composant, ce qui est génial.

Plusieurs classes de factory

Alternativement, vous pouvez créer plusieurs classes pour chaque formulaire que vous souhaitez utiliser dans votre application. Cette approche peut améliorer la lisibilité du code et faciliter la gestion des formulaires. Nous laisserons la FormFactory originale créer uniquement un formulaire propre avec une configuration de base (par exemple, avec prise en charge des traductions) et pour le formulaire d'édition, nous créerons une nouvelle factory EditFormFactory.

class FormFactory
{
	public function __construct(
		private Translator $translator,
	) {
	}

	public function create(): Form
	{
		$form = new Form;
		$form->setTranslator($this->translator);
		return $form;
	}
}


// ✅ utilisation de la composition
class EditFormFactory
{
	public function __construct(
		private FormFactory $formFactory,
	) {
	}

	public function create(): Form
	{
		$form = $this->formFactory->create();
		// ici, on ajoute d'autres champs de formulaire
		$form->addSubmit('send', 'Envoyer');
		return $form;
	}
}

Il est très important que la liaison entre les classes FormFactory et EditFormFactory soit réalisée par composition, et non par héritage objet :

// ⛔ PAS COMME ÇA ! L'HÉRITAGE N'A PAS SA PLACE ICI
class EditFormFactory extends FormFactory
{
	public function create(): Form
	{
		$form = parent::create();
		$form->addText('title', 'Titre :');
		// ici, on ajoute d'autres champs de formulaire
		$form->addSubmit('send', 'Envoyer');
		return $form;
	}
}

L'utilisation de l'héritage serait dans ce cas totalement contre-productive. Vous rencontreriez des problèmes très rapidement. Par exemple, au moment où vous voudriez ajouter des paramètres à la méthode create() ; PHP signalerait une erreur indiquant que sa signature diffère de celle du parent. Ou lors de la transmission de dépendances à la classe EditFormFactory via le constructeur. Une situation appelée enfer du constructeur se produirait.

En général, il est préférable de privilégier la composition plutôt que l'héritage.

Gestion du formulaire

La gestion du formulaire, qui est appelée après une soumission réussie, peut également faire partie de la classe factory. Elle fonctionnera en transmettant les données soumises au modèle pour traitement. Les erreurs éventuelles seront retournées au formulaire. Le modèle dans l'exemple suivant est représenté par la classe Facade :

class EditFormFactory
{
	public function __construct(
		private FormFactory $formFactory,
		private Facade $facade,
	) {
	}

	public function create(): Form
	{
		$form = $this->formFactory->create();
		$form->addText('title', 'Titre :');
		// ici, on ajoute d'autres champs de formulaire
		$form->addSubmit('send', 'Envoyer');
		$form->onSuccess[] = [$this, 'processForm'];
		return $form;
	}

	public function processForm(Form $form, array $data): void
	{
		try {
			// traitement des données soumises
			$this->facade->process($data);

		} catch (AnyModelException $e) {
			$form->addError('...');
		}
	}
}

Cependant, nous laisserons la redirection elle-même au presenter. Celui-ci ajoutera un autre gestionnaire à l'événement onSuccess, qui effectuera la redirection. Grâce à cela, il sera possible d'utiliser le formulaire dans différents presenters et de rediriger différemment dans chacun d'eux.

class MyPresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private EditFormFactory $formFactory,
	) {
	}

	protected function createComponentEditForm(): Form
	{
		$form = $this->formFactory->create();
		$form->onSuccess[] = function () {
			$this->flashMessage('L\'enregistrement a été sauvegardé');
			$this->redirect('Homepage:');
		};
		return $form;
	}
}

Cette solution utilise la propriété des formulaires selon laquelle si addError() est appelé sur le formulaire ou l'un de ses éléments, le gestionnaire onSuccess suivant n'est plus appelé.

Héritage de la classe Form

Un formulaire assemblé ne doit pas être un descendant du formulaire. En d'autres termes, n'utilisez pas cette solution :

// ⛔ PAS COMME ÇA ! L'HÉRITAGE N'A PAS SA PLACE ICI
class EditForm extends Form
{
	public function __construct(Translator $translator)
	{
		parent::__construct();
		$this->addText('title', 'Titre :');
		// ici, on ajoute d'autres champs de formulaire
		$this->addSubmit('send', 'Envoyer');
		$this->setTranslator($translator);
	}
}

Au lieu d'assembler le formulaire dans le constructeur, utilisez une factory.

Il faut comprendre que la classe Form est avant tout un outil pour assembler un formulaire, c'est-à-dire un form builder. Et le formulaire assemblé peut être considéré comme son produit. Or, le produit n'est pas un cas spécifique du builder, il n'y a pas entre eux de relation is a qui constitue la base de l'héritage.

Composant avec formulaire

Une approche totalement différente consiste à créer un composant qui inclut un formulaire. Cela offre de nouvelles possibilités, par exemple rendre le formulaire d'une manière spécifique, car le composant inclut également un template. Ou bien, on peut utiliser des signaux pour la communication AJAX et le chargement différé d'informations dans le formulaire, par exemple pour l'autocomplétion, etc.

use Nette\Application\UI\Form;

class EditControl extends Nette\Application\UI\Control
{
	public array $onSave = [];

	public function __construct(
		private Facade $facade,
	) {
	}

	protected function createComponentForm(): Form
	{
		$form = new Form;
		$form->addText('title', 'Titre :');
		// ici, on ajoute d'autres champs de formulaire
		$form->addSubmit('send', 'Envoyer');
		$form->onSuccess[] = [$this, 'processForm'];

		return $form;
	}

	public function processForm(Form $form, array $data): void
	{
		try {
			// traitement des données soumises
			$this->facade->process($data);

		} catch (AnyModelException $e) {
			$form->addError('...');
			return;
		}

		// déclenchement de l'événement
		$this->onSave($this, $data);
	}
}

Nous allons également créer une factory qui produira ce composant. Il suffit d'écrire son interface :

interface EditControlFactory
{
	function create(): EditControl;
}

Et l'ajouter au fichier de configuration :

services:
	- EditControlFactory

Et maintenant, nous pouvons demander la factory et l'utiliser dans le presenter :

class MyPresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private EditControlFactory $controlFactory,
	) {
	}

	protected function createComponentEditForm(): EditControl
	{
		$control = $this->controlFactory->create();

		$control->onSave[] = function (EditControl $control, $data) {
			$this->redirect('this');
			// ou nous redirigeons vers le résultat de l'édition, par ex. :
			// $this->redirect('detail', ['id' => $data->id]);
		};

		return $control;
	}
}