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