Réutiliser les formulaires à plusieurs endroits

Nette propose plusieurs façons de réutiliser le même formulaire à plusieurs endroits sans dupliquer de code. Cet article passera en revue différentes solutions, y compris celles que vous devriez éviter.

Factory de formulaire

Une approche fondamentale pour réutiliser un composant à plusieurs endroits consiste à créer une méthode ou une classe qui génère ce composant. Cette méthode est ensuite appelée depuis différents endroits de l'application. Une telle méthode ou classe s'appelle une factory. Ne la confondez pas avec le patron de conception factory method, qui décrit une façon particulière d'utiliser les factories et n'a pas de rapport direct avec ce sujet.

Créons par exemple une factory qui construit un formulaire d'édition :

use Nette\Application\UI\Form;

class FormFactory
{
	public function createEditForm(): Form
	{
		$form = new Form;
		$form->addText('title', 'Titre :');
		// d'autres champs du formulaire sont ajoutés ici
		$form->addSubmit('send', 'Enregistrer');
		return $form;
	}
}

Vous pouvez maintenant utiliser cette factory dans différentes parties de votre application, comme les presenters ou les composants. Pour cela, vous la demandez comme dépendance. Enregistrez d'abord la classe dans le fichier de configuration :

services:
	- FormFactory

Puis utilisez-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 étoffer la factory de formulaire avec d'autres méthodes créant les types de formulaires dont votre application a besoin. Et naturellement, nous pouvons ajouter une méthode qui crée un formulaire de base sans éléments, que les autres méthodes pourront ensuite utiliser :

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

	public function createEditForm(): Form
	{
		$form = $this->createForm();
		$form->addText('title', 'Titre :');
		// d'autres champs du formulaire sont ajoutés ici
		$form->addSubmit('send', 'Enregistrer');
		return $form;
	}
}

La méthode createForm() ne fait encore rien de bien utile, mais cela va changer très vite.

Dépendances de la factory

Avec le temps, il peut devenir nécessaire que les formulaires soient multilingues. Cela signifie définir un traducteur pour tous les formulaires. Pour y parvenir, modifiez la classe FormFactory pour qu'elle reçoive l'objet Translator comme dépendance dans son constructeur et le passe au formulaire créé :

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 aussi appelée par les autres méthodes qui créent des formulaires précis, définir le traducteur ici suffit. Et c'est terminé. Il n'y a besoin de modifier le code d'aucun presenter ni composant, ce qui est excellent.

Plusieurs classes factory

Vous pouvez aussi créer des classes factory distinctes pour chaque formulaire que vous comptez utiliser dans votre application. Cette approche peut améliorer la lisibilité du code et simplifier la gestion des formulaires. Laissez la FormFactory d'origine ne créer qu'un formulaire de base avec la configuration fondamentale (comme la prise en charge de la traduction) et créez une nouvelle factory, EditFormFactory, spécialement pour le formulaire d'édition.

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();
		// d'autres champs du formulaire sont ajoutés ici
		$form->addSubmit('send', 'Enregistrer');
		return $form;
	}
}

Il est essentiel que la relation entre les classes FormFactory et EditFormFactory soit réalisée par composition, et non par héritage d'objets :

// ⛔ NON ! L'HÉRITAGE N'A PAS SA PLACE ICI
class EditFormFactory extends FormFactory
{
	public function create(): Form
	{
		$form = parent::create();
		$form->addText('title', 'Titre :');
		// d'autres champs du formulaire sont ajoutés ici
		$form->addSubmit('send', 'Enregistrer');
		return $form;
	}
}

Utiliser ici l'héritage serait tout à fait contre-productif. Vous rencontreriez des problèmes très vite. Par exemple, si vous vouliez ajouter des paramètres à la méthode create(), PHP signalerait une erreur, car sa signature différerait de celle du parent. Ou encore lors du passage de dépendances à la classe EditFormFactory par le constructeur. Cela mènerait à ce qu'on appelle le constructor hell.

D'une manière générale, il vaut mieux préférer la composition à l'héritage.

Traitement du formulaire

Le gestionnaire du formulaire, invoqué après une soumission réussie, peut lui aussi faire partie de la classe factory. Il fonctionne en passant les données soumises à la couche modèle pour traitement. Les éventuelles erreurs de traitement sont renvoyées au formulaire. Dans l'exemple suivant, le modèle 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 :');
		// d'autres champs du formulaire sont ajoutés ici
		$form->addSubmit('send', 'Enregistrer');
		$form->onSuccess[] = $this->processForm(...);
		return $form;
	}

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

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

Laissez cependant le presenter s'occuper lui-même de la redirection. Il ajoute à l'événement onSuccess un gestionnaire supplémentaire qui effectue la redirection. Le formulaire peut ainsi être utilisé dans différents presenters, chacun redirigeant en cas de succès vers un endroit différent.

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 tire parti d'une caractéristique des formulaires : si addError() est appelée sur le formulaire ou sur l'un de ses éléments, les gestionnaires onSuccess suivants ne sont pas invoqués.

Hériter de la classe Form

Un formulaire assemblé ne devrait pas être un descendant de la classe Form. Autrement dit, évitez cette approche :

// ⛔ NON ! L'HÉRITAGE N'A PAS SA PLACE ICI
class EditForm extends Form
{
	public function __construct(Translator $translator)
	{
		parent::__construct();
		$this->addText('title', 'Titre :');
		// d'autres champs du formulaire sont ajoutés ici
		$this->addSubmit('send', 'Enregistrer');
		$this->setTranslator($translator);
	}
}

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

Il est important de comprendre que la classe Form est avant tout un outil de construction de formulaires, autrement dit un form builder. Le formulaire assemblé peut être considéré comme son produit. Or un produit n'est pas un type particulier de constructeur ; il n'y a pas de relation est un entre eux, qui est le fondement de l'héritage.

Composant formulaire

Une approche complètement différente consiste à créer un composant qui encapsule le formulaire. Cela ouvre de nouvelles possibilités, comme rendre le formulaire d'une manière particulière, puisque le composant possède son propre template. On peut aussi utiliser les signaux pour la communication AJAX et charger dynamiquement des informations dans le formulaire, par exemple pour des suggestions, 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 :');
		// d'autres champs du formulaire sont ajoutés ici
		$form->addSubmit('send', 'Enregistrer');
		$form->onSuccess[] = $this->processForm(...);

		return $form;
	}

	private 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);
	}
}

Créons ensuite une factory qui produira ce composant. Il suffit d'en définir l'interface :

interface EditControlFactory
{
	function create(): EditControl;
}

Et de l'ajouter au fichier de configuration :

services:
	- EditControlFactory

Nous pouvons maintenant 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 redirection vers le résultat de l'édition, par exemple :
			// $this->redirect('detail', ['id' => $data->id]);
		};

		return $control;
	}
}