Επαναχρησιμοποίηση φορμών σε πολλαπλά μέρη
Στο Nette έχετε στη διάθεσή σας αρκετές επιλογές για να χρησιμοποιήσετε την ίδια φόρμα σε πολλαπλά μέρη και να μην επαναλαμβάνετε τον κώδικα. Σε αυτό το άρθρο θα δείξουμε διάφορες λύσεις, συμπεριλαμβανομένων εκείνων που θα έπρεπε να αποφύγετε.
Factory φορμών
Μία από τις βασικές προσεγγίσεις για τη χρήση του ίδιου component σε πολλαπλά μέρη είναι η δημιουργία μιας μεθόδου ή κλάσης που παράγει αυτό το component, και στη συνέχεια η κλήση αυτής της μεθόδου σε διάφορα μέρη της εφαρμογής. Μια τέτοια μέθοδος ή κλάση ονομάζεται factory. Μην τη συγχέετε με το design pattern factory method, το οποίο περιγράφει έναν συγκεκριμένο τρόπο χρήσης των factories και δεν σχετίζεται με αυτό το θέμα.
Ως παράδειγμα, θα δημιουργήσουμε ένα factory που θα κατασκευάζει μια φόρμα επεξεργασίας:
use Nette\Application\UI\Form;
class FormFactory
{
public function createEditForm(): Form
{
$form = new Form;
$form->addText('title', 'Τίτλος:');
// εδώ προστίθενται άλλα πεδία φόρμας
$form->addSubmit('send', 'Αποστολή');
return $form;
}
}
Τώρα μπορείτε να χρησιμοποιήσετε αυτό το factory σε διάφορα μέρη της εφαρμογής σας, για παράδειγμα σε presenters ή components. Και αυτό γίνεται ζητώντας το ως εξάρτηση. Πρώτα, λοιπόν, καταχωρούμε την κλάση στο αρχείο διαμόρφωσης:
services:
- FormFactory
Και στη συνέχεια τη χρησιμοποιούμε στον 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 () {
// επεξεργασία των απεσταλμένων δεδομένων
};
return $form;
}
}
Μπορείτε να επεκτείνετε το factory φορμών με επιπλέον μεθόδους για τη δημιουργία άλλων τύπων φορμών ανάλογα με τις ανάγκες της εφαρμογής σας. Και φυσικά, μπορούμε να προσθέσουμε και μια μέθοδο που δημιουργεί μια βασική φόρμα χωρίς στοιχεία, και αυτή θα χρησιμοποιείται από τις άλλες μεθόδους:
class FormFactory
{
public function createForm(): Form
{
$form = new Form;
return $form;
}
public function createEditForm(): Form
{
$form = $this->createForm();
$form->addText('title', 'Τίτλος:');
// εδώ προστίθενται άλλα πεδία φόρμας
$form->addSubmit('send', 'Αποστολή');
return $form;
}
}
Η μέθοδος createForm()
προς το παρόν δεν κάνει τίποτα χρήσιμο, αλλά
αυτό θα αλλάξει γρήγορα.
Εξαρτήσεις του factory
Με τον καιρό θα φανεί ότι χρειαζόμαστε οι φόρμες να είναι
πολυγλωσσικές. Αυτό σημαίνει ότι σε όλες τις φόρμες πρέπει να ορίσουμε
τον λεγόμενο translator. Για τον σκοπό
αυτό, τροποποιούμε την κλάση FormFactory
ώστε να δέχεται το
αντικείμενο Translator
ως εξάρτηση στον constructor, και το περνάμε
στη φόρμα:
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;
}
// ...
}
Επειδή η μέθοδος createForm()
καλείται και από τις άλλες μεθόδους
που δημιουργούν συγκεκριμένες φόρμες, αρκεί να ορίσουμε τον translator μόνο
σε αυτήν. Και τελειώσαμε. Δεν χρειάζεται να αλλάξουμε τον κώδικα
κανενός presenter ή component, πράγμα που είναι εξαιρετικό.
Περισσότερες κλάσεις factory
Εναλλακτικά, μπορείτε να δημιουργήσετε περισσότερες κλάσεις για
κάθε φόρμα που θέλετε να χρησιμοποιήσετε στην εφαρμογή σας. Αυτή η
προσέγγιση μπορεί να αυξήσει την αναγνωσιμότητα του κώδικα και να
διευκολύνει τη διαχείριση των φορμών. Το αρχικό FormFactory
θα το
αφήσουμε να δημιουργεί μόνο μια καθαρή φόρμα με βασική διαμόρφωση (για
παράδειγμα με υποστήριξη μεταφράσεων) και για τη φόρμα επεξεργασίας θα
δημιουργήσουμε ένα νέο factory EditFormFactory
.
class FormFactory
{
public function __construct(
private Translator $translator,
) {
}
public function create(): Form
{
$form = new Form;
$form->setTranslator($this->translator);
return $form;
}
}
// ✅ χρήση σύνθεσης
class EditFormFactory
{
public function __construct(
private FormFactory $formFactory,
) {
}
public function create(): Form
{
$form = $this->formFactory->create();
// εδώ προστίθενται άλλα πεδία φόρμας
$form->addSubmit('send', 'Αποστολή');
return $form;
}
}
Είναι πολύ σημαντικό η σχέση μεταξύ των κλάσεων FormFactory
και
EditFormFactory
να υλοποιείται με σύνθεση, και όχι με
κληρονομικότητα
αντικειμένων:
// ⛔ ΟΧΙ ΕΤΣΙ! Η ΚΛΗΡΟΝΟΜΙΚΟΤΗΤΑ ΔΕΝ ΑΝΗΚΕΙ ΕΔΩ
class EditFormFactory extends FormFactory
{
public function create(): Form
{
$form = parent::create();
$form->addText('title', 'Τίτλος:');
// εδώ προστίθενται άλλα πεδία φόρμας
$form->addSubmit('send', 'Αποστολή');
return $form;
}
}
Η χρήση κληρονομικότητας θα ήταν σε αυτή την περίπτωση εντελώς
αντιπαραγωγική. Θα αντιμετωπίζατε προβλήματα πολύ γρήγορα. Για
παράδειγμα, τη στιγμή που θα θέλατε να προσθέσετε παραμέτρους στη
μέθοδο create()
; η PHP θα ανέφερε σφάλμα ότι η υπογραφή της διαφέρει
από την γονική. Ή κατά το πέρασμα εξαρτήσεων στην κλάση EditFormFactory
μέσω του constructor. Θα προέκυπτε η κατάσταση που ονομάζουμε constructor hell.
Γενικά, είναι καλύτερο να προτιμάτε τη σύνθεση έναντι κληρονομικότητας.
Χειρισμός φόρμας
Ο χειρισμός της φόρμας, που καλείται μετά την επιτυχή υποβολή, μπορεί
επίσης να είναι μέρος της κλάσης factory. Θα λειτουργεί έτσι ώστε να
παραδίδει τα υποβληθέντα δεδομένα στο μοντέλο για επεξεργασία. Τυχόν
σφάλματα θα τα επιστρέψει
στη φόρμα. Το μοντέλο στο ακόλουθο παράδειγμα αντιπροσωπεύεται από την
κλάση Facade
:
class EditFormFactory
{
public function __construct(
private FormFactory $formFactory,
private Facade $facade,
) {
}
public function create(): Form
{
$form = $this->formFactory->create();
$form->addText('title', 'Τίτλος:');
// εδώ προστίθενται άλλα πεδία φόρμας
$form->addSubmit('send', 'Αποστολή');
$form->onSuccess[] = [$this, 'processForm'];
return $form;
}
public function processForm(Form $form, array $data): void
{
try {
// επεξεργασία των απεσταλμένων δεδομένων
$this->facade->process($data);
} catch (AnyModelException $e) {
$form->addError('...');
}
}
}
Την ίδια την ανακατεύθυνση όμως θα την αφήσουμε στον presenter. Αυτός θα
προσθέσει στο event onSuccess
έναν επιπλέον handler που θα
πραγματοποιήσει την ανακατεύθυνση. Χάρη σε αυτό, θα είναι δυνατό να
χρησιμοποιηθεί η φόρμα σε διάφορους presenters και σε καθέναν να γίνει
ανακατεύθυνση αλλού.
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('Η εγγραφή αποθηκεύτηκε');
$this->redirect('Homepage:');
};
return $form;
}
}
Αυτή η λύση εκμεταλλεύεται την ιδιότητα των φορμών ότι όταν καλείται
το addError()
πάνω στη φόρμα ή στα στοιχεία της, ο επόμενος handler
onSuccess
δεν καλείται πλέον.
Κληρονομικότητα από την κλάση Form
Η συναρμολογημένη φόρμα δεν πρέπει να είναι απόγονος της φόρμας. Με άλλα λόγια, μην χρησιμοποιείτε αυτή τη λύση:
// ⛔ ΟΧΙ ΕΤΣΙ! Η ΚΛΗΡΟΝΟΜΙΚΟΤΗΤΑ ΔΕΝ ΑΝΗΚΕΙ ΕΔΩ
class EditForm extends Form
{
public function __construct(Translator $translator)
{
parent::__construct();
$this->addText('title', 'Τίτλος:');
// εδώ προστίθενται άλλα πεδία φόρμας
$this->addSubmit('send', 'Αποστολή');
$this->setTranslator($translator);
}
}
Αντί να συναρμολογείτε τη φόρμα στον constructor, χρησιμοποιήστε ένα factory.
Πρέπει να συνειδητοποιήσετε ότι η κλάση Form
είναι πρωτίστως
ένα εργαλείο για τη συναρμολόγηση μιας φόρμας, δηλαδή ένας form builder.
Και η συναρμολογημένη φόρμα μπορεί να θεωρηθεί ως προϊόν της. Όμως το
προϊόν δεν είναι μια ειδική περίπτωση του builder, δεν υπάρχει μεταξύ τους
σχέση is a που αποτελεί τη βάση της κληρονομικότητας.
Component με φόρμα
Μια εντελώς διαφορετική προσέγγιση είναι η δημιουργία ενός component, μέρος του οποίου είναι μια φόρμα. Αυτό δίνει νέες δυνατότητες, για παράδειγμα την απόδοση της φόρμας με συγκεκριμένο τρόπο, καθώς μέρος του component είναι και ένα template. Ή μπορεί να χρησιμοποιηθούν σήματα για επικοινωνία AJAX και φόρτωση πληροφοριών στη φόρμα, για παράδειγμα για προτάσεις, κ.λπ.
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', 'Τίτλος:');
// εδώ προστίθενται άλλα πεδία φόρμας
$form->addSubmit('send', 'Αποστολή');
$form->onSuccess[] = [$this, 'processForm'];
return $form;
}
public function processForm(Form $form, array $data): void
{
try {
// επεξεργασία των απεσταλμένων δεδομένων
$this->facade->process($data);
} catch (AnyModelException $e) {
$form->addError('...');
return;
}
// εκκίνηση του event
$this->onSave($this, $data);
}
}
Θα δημιουργήσουμε επίσης ένα factory που θα παράγει αυτό το component. Αρκεί να καταχωρήσετε το interface του:
interface EditControlFactory
{
function create(): EditControl;
}
Και να το προσθέσουμε στο αρχείο διαμόρφωσης:
services:
- EditControlFactory
Και τώρα μπορούμε ήδη να ζητήσουμε το factory και να το χρησιμοποιήσουμε στον 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');
// ή ανακατευθύνουμε στο αποτέλεσμα της επεξεργασίας, π.χ.:
// $this->redirect('detail', ['id' => $data->id]);
};
return $control;
}
}